chad-code 1.0.1__tar.gz → 1.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-1.0.1/src/chad_code.egg-info → chad_code-1.0.3}/PKG-INFO +15 -15
- {chad_code-1.0.1 → chad_code-1.0.3}/README.md +14 -14
- {chad_code-1.0.1 → chad_code-1.0.3}/pyproject.toml +1 -1
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/__init__.py +1 -1
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/agent.py +150 -20
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/cli.py +17 -2
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/guardrails.py +256 -43
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/levers.py +40 -6
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/prompt.py +15 -1
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/syntaxgate.py +25 -4
- {chad_code-1.0.1 → chad_code-1.0.3/src/chad_code.egg-info}/PKG-INFO +15 -15
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/SOURCES.txt +1 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent_e2e.py +40 -8
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent_guards.py +165 -4
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_done_audit.py +97 -0
- chad_code-1.0.3/tests/test_garble_invariant.py +86 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_intent.py +27 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_lever_bite.py +53 -6
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_syntaxgate.py +25 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/LICENSE +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/setup.cfg +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/atif.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/base_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/bench.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/compaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/completion_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/config.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/diag.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/ignore.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/lsp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mcp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mcp_oauth.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/profiles.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/prove.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/render.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/repomap.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/session.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/skills.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/symbols.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/toolcall_parse.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/tools.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/tui.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad/validate.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_agent.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_atif.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_bench.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_cli.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_compact_notice.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_compaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_completion_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_config.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_confirm_preview.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_drift_warn.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_edit.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_edit_corruption.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_engine_kvquant.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_feel_pack.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_gate.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_ignore.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_levers.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_log_redaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_lsp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mcp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mcp_oauth.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_plan_review.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_prove.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_render.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_replace_lines.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_repomap.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_session.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_skills.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_subagent.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_symbols.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_toolcall_parse.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_tools.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.3}/tests/test_tui.py +0 -0
- {chad_code-1.0.1 → chad_code-1.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: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
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.3"
|
|
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"
|
|
@@ -786,6 +786,13 @@ class Agent:
|
|
|
786
786
|
gate_nudges = 0 # times the investigation->edit gate fired this turn
|
|
787
787
|
subagent_sigs = set() # (description, prompt) of sub-agents already spawned this turn
|
|
788
788
|
truncation_nudges = 0 # times we pushed past a token-cap truncation this turn
|
|
789
|
+
garble_nudges = 0 # malformed-tool-call re-nudges (own counter, so a step-0
|
|
790
|
+
# cap-hit can't spend the garble recovery budget)
|
|
791
|
+
consecutive_garbles = 0 # back-to-back garbled steps (drives exemplar + scrub)
|
|
792
|
+
last_garble_idx = None # self.messages index of the previous garbled assistant
|
|
793
|
+
# message (scrubbed if the next step garbles too)
|
|
794
|
+
done_audit_bounces = 0 # total audit bounces this turn (hard cap 2)
|
|
795
|
+
audit_absent_list = [] # task-named paths the FIRST audit stat'ed as absent
|
|
789
796
|
landing_nudges = 0 # one-shot "you're out of steps, land the edit" near the cap
|
|
790
797
|
consecutive_failed_bash = 0 # back-to-back errored bash with no edit (thrash)
|
|
791
798
|
thrash_nudges = 0
|
|
@@ -807,12 +814,18 @@ class Agent:
|
|
|
807
814
|
# Turn-level cumulative think budget: distinct from self.think_tokens
|
|
808
815
|
# (lifetime across the whole Agent instance, used for cross-turn telemetry) — this
|
|
809
816
|
# is THIS turn's spend, reset every run_turn call, checked against a wall- and
|
|
810
|
-
# decode-speed-aware budget. turn_think_exhausted
|
|
811
|
-
#
|
|
812
|
-
#
|
|
817
|
+
# decode-speed-aware budget. turn_think_exhausted latches the once-only
|
|
818
|
+
# log/steer; past it, no-think steps are paid on a duty cycle
|
|
819
|
+
# (guardrails.turn_think_throttle) rather than muting the rest of the turn —
|
|
820
|
+
# the blanket mute regressed run1 passes with garbled no-think tails (plan 107).
|
|
821
|
+
# landing_no_think is the 103 landing's own unconditional latch: once the hard
|
|
822
|
+
# wrap-up fires, the landing and everything after it stay no-think regardless.
|
|
813
823
|
turn_think_tokens = 0
|
|
814
824
|
turn_think_half_fired = False
|
|
815
825
|
turn_think_exhausted = False
|
|
826
|
+
turn_think_nothink_paid = 0
|
|
827
|
+
turn_think_budget_now = 0
|
|
828
|
+
landing_no_think = False
|
|
816
829
|
# Runaway-turn governor state. turn_start drives the optional wall
|
|
817
830
|
# budget; gov_band is the highest budget checkpoint already evaluated; gov_progress
|
|
818
831
|
# tracks whether a change landed+verified within the CURRENT band (resets each time
|
|
@@ -845,6 +858,12 @@ class Agent:
|
|
|
845
858
|
# where it also selects the plan-mode prefix.)
|
|
846
859
|
action_task = self.mode != "plan" and _intent["action"]
|
|
847
860
|
read_only_intent = _intent["read_only"]
|
|
861
|
+
# Run-task intent (start/boot/serve/… — system-state imperatives with no file
|
|
862
|
+
# deliverable): arms the anti-bail nudges alongside action_task but is kept OUT
|
|
863
|
+
# of the no-empty-diff done gates below, which demand a landed edit a run task
|
|
864
|
+
# legitimately never makes (plan 107 follow-up: qemu-startup classified as
|
|
865
|
+
# neither, so a prose give-up with 81% of the wall left took the weakest path).
|
|
866
|
+
run_task = self.mode != "plan" and _intent.get("run", False)
|
|
848
867
|
# Progress-aware step cap (see guardrails.extend_step_cap): max_steps is the
|
|
849
868
|
# WINDOW size, not a hard kill. A window that landed+verified a change earns
|
|
850
869
|
# another window (warm cache — no re-prefill, unlike a governor rollover); a
|
|
@@ -963,9 +982,19 @@ class Agent:
|
|
|
963
982
|
# <think> off so the model must act, then the flag clears and thinking restores.
|
|
964
983
|
# Consumes the one-shot here; the render/decode/accounting below all key off
|
|
965
984
|
# `step_thinking` rather than self.thinking for exactly this step.
|
|
966
|
-
# Turn-level think budget:
|
|
967
|
-
#
|
|
968
|
-
|
|
985
|
+
# Turn-level think budget: past exhaustion, forced no-think steps are
|
|
986
|
+
# paid on a duty cycle (one per TURN_THINK_REARM_TOK further think tokens)
|
|
987
|
+
# so thinking RESTORES once the model stops over-spending — a blanket
|
|
988
|
+
# rest-of-turn mute regressed run1 passes (plan 107 F1). The 103 landing's
|
|
989
|
+
# landing_no_think stays unconditional.
|
|
990
|
+
_tt_throttled = (turn_think_exhausted
|
|
991
|
+
and guardrails.turn_think_throttle(
|
|
992
|
+
turn_think_tokens, turn_think_budget_now,
|
|
993
|
+
turn_think_nothink_paid))
|
|
994
|
+
if _tt_throttled:
|
|
995
|
+
turn_think_nothink_paid += 1
|
|
996
|
+
step_thinking = (self.thinking and not landing_no_think
|
|
997
|
+
and not _tt_throttled)
|
|
969
998
|
if no_think_next:
|
|
970
999
|
no_think_next = False
|
|
971
1000
|
capped_stall_streak = 0
|
|
@@ -1203,9 +1232,15 @@ class Agent:
|
|
|
1203
1232
|
# turns (a Q&A turn shouldn't be pushed to "act now") and without a configured
|
|
1204
1233
|
# wall budget (interactive/unmetered runs, like wrapup_window above).
|
|
1205
1234
|
turn_think_tokens += _think_delta
|
|
1206
|
-
|
|
1235
|
+
# Inert below TURN_THINK_MIN_WALL_S: a short auto-continue tail clamps to
|
|
1236
|
+
# the LO budget and half-fires on its first step, churning against
|
|
1237
|
+
# hard_wrapup's landing (plan 107 F2 — the regex-log relaunch signature).
|
|
1238
|
+
if (self._turn_budget_s
|
|
1239
|
+
and self._turn_budget_s >= guardrails.TURN_THINK_MIN_WALL_S
|
|
1240
|
+
and self.mode != "plan" and not read_only_intent
|
|
1207
1241
|
and levers.enabled("turn_think_budget")):
|
|
1208
1242
|
_tt_budget = guardrails.turn_think_budget(self._turn_budget_s, self.tok_per_s)
|
|
1243
|
+
turn_think_budget_now = _tt_budget
|
|
1209
1244
|
_tt_decision, turn_think_half_fired, turn_think_exhausted = (
|
|
1210
1245
|
guardrails.turn_think_budget_check(
|
|
1211
1246
|
turn_think_tokens, _tt_budget,
|
|
@@ -1217,10 +1252,11 @@ class Agent:
|
|
|
1217
1252
|
"content": guardrails.TURN_THINK_BUDGET_STEER})
|
|
1218
1253
|
elif _tt_decision == "exhausted":
|
|
1219
1254
|
log.info("THINK-BUDGET exhausted at step %d: %d/%d cumulative think "
|
|
1220
|
-
"tok this turn —
|
|
1221
|
-
turn_think_tokens,
|
|
1255
|
+
"tok this turn — throttling <think> (one action step per "
|
|
1256
|
+
"%d further think tok)", step, turn_think_tokens,
|
|
1257
|
+
_tt_budget, guardrails.TURN_THINK_REARM_TOK)
|
|
1222
1258
|
self._emit("info", " [reasoning budget exhausted for this turn — "
|
|
1223
|
-
"
|
|
1259
|
+
"throttling further <think>]")
|
|
1224
1260
|
|
|
1225
1261
|
log.info("step %d: %d tok @ %.1f tok/s | prefill %d new + %d cached | "
|
|
1226
1262
|
"accept %.2f", step, stats.generated_tokens, stats.tok_per_s,
|
|
@@ -1277,7 +1313,7 @@ class Agent:
|
|
|
1277
1313
|
# above) with its tokens in the KV cache. Force ONE time-boxed, no-think
|
|
1278
1314
|
# landing turn so the model writes its best artifacts to the exact paths before
|
|
1279
1315
|
# the harness kills the turn at the wall — instead of dying mid-token with
|
|
1280
|
-
# nothing landed.
|
|
1316
|
+
# nothing landed. landing_no_think makes the landing (and any step after it)
|
|
1281
1317
|
# no-think so it can't re-open a spiral; the latch disarms _deadline_stop, so
|
|
1282
1318
|
# the landing generation runs to its token box, not another abort. deadline_fired
|
|
1283
1319
|
# is exclusive with rep_fired / the think-cap below (it is checked first in
|
|
@@ -1286,7 +1322,7 @@ class Agent:
|
|
|
1286
1322
|
# there is rarely one, and partial-tool-call surgery is not worth its risk here.
|
|
1287
1323
|
if deadline_fired[0] and not hard_wrapup_fired:
|
|
1288
1324
|
hard_wrapup_fired = True
|
|
1289
|
-
|
|
1325
|
+
landing_no_think = True
|
|
1290
1326
|
_remaining = self._turn_budget_s - (time.monotonic() - turn_start)
|
|
1291
1327
|
log.info("HARD-WRAPUP abort at step %d: %.0fs left, gen was %d tok",
|
|
1292
1328
|
step, _remaining, stats.generated_tokens)
|
|
@@ -1371,12 +1407,33 @@ class Agent:
|
|
|
1371
1407
|
_stripped_for_markers = strip_think(text)
|
|
1372
1408
|
garbled = ("<tool_call>" in _stripped_for_markers
|
|
1373
1409
|
or "</function>" in _stripped_for_markers
|
|
1374
|
-
or "<function=" in _stripped_for_markers
|
|
1410
|
+
or "<function=" in _stripped_for_markers
|
|
1411
|
+
or _has_open_tool_call(text))
|
|
1412
|
+
if garbled:
|
|
1413
|
+
consecutive_garbles += 1
|
|
1414
|
+
# From the 2nd consecutive garble, scrub the PREVIOUS garbled
|
|
1415
|
+
# assistant body: the wrong dialect must not stay in context as a
|
|
1416
|
+
# few-shot example (repeated garbles in a row each condition on the
|
|
1417
|
+
# last). Costs one prefix-cache invalidation on this rare path.
|
|
1418
|
+
if (levers.enabled("garble_never_final")
|
|
1419
|
+
and consecutive_garbles >= 2 and last_garble_idx is not None):
|
|
1420
|
+
self.messages[last_garble_idx]["content"] = \
|
|
1421
|
+
guardrails.GARBLE_SCRUBBED
|
|
1422
|
+
log.info("GARBLE scrub at step %d: previous garbled message "
|
|
1423
|
+
"body removed from context (consecutive=%d)",
|
|
1424
|
+
step, consecutive_garbles)
|
|
1425
|
+
last_garble_idx = len(self.messages) - 1
|
|
1426
|
+
else:
|
|
1427
|
+
consecutive_garbles = 0
|
|
1375
1428
|
kind, nudge = guardrails.nudge_for_no_calls(
|
|
1376
1429
|
text, hit_cap, made_edit, unverified_edit, read_only_intent,
|
|
1377
|
-
action_task, truncation_nudges, answer_nudges,
|
|
1378
|
-
_has_open_tool_call(text), garbled_call=garbled
|
|
1379
|
-
|
|
1430
|
+
action_task or run_task, truncation_nudges, answer_nudges,
|
|
1431
|
+
verify_nudges, _has_open_tool_call(text), garbled_call=garbled,
|
|
1432
|
+
garble_nudges=garble_nudges,
|
|
1433
|
+
consecutive_garbles=consecutive_garbles)
|
|
1434
|
+
if kind == "garble":
|
|
1435
|
+
garble_nudges += 1
|
|
1436
|
+
elif kind == "truncated":
|
|
1380
1437
|
truncation_nudges += 1
|
|
1381
1438
|
elif kind == "no-edit":
|
|
1382
1439
|
answer_nudges += 1
|
|
@@ -1384,9 +1441,25 @@ class Agent:
|
|
|
1384
1441
|
verify_nudges += 1
|
|
1385
1442
|
if nudge:
|
|
1386
1443
|
log.info("END-ANSWER rejected step %d: %s (hit_cap=%s, has_code=%s, "
|
|
1387
|
-
"action_task=%s)", step, kind, hit_cap,
|
|
1444
|
+
"action_task=%s, run_task=%s)", step, kind, hit_cap,
|
|
1445
|
+
has_code, action_task, run_task)
|
|
1388
1446
|
self.messages.append({"role": "tool", "name": "edit", "content": nudge})
|
|
1389
1447
|
continue
|
|
1448
|
+
# Invariant: a step whose text contains tool-call markers is NEVER a
|
|
1449
|
+
# final answer — the model was trying to act, not to finish. With the
|
|
1450
|
+
# nudge budget spent, hard-stop with a banked note instead of accepting
|
|
1451
|
+
# the garble (otherwise a run of garbles can burn the shared counter and
|
|
1452
|
+
# the audit latch, and the last one ships as the answer with most of the
|
|
1453
|
+
# wall budget still unspent).
|
|
1454
|
+
if garbled and levers.enabled("garble_never_final"):
|
|
1455
|
+
self.budget_note = guardrails.progress_note(self.messages)
|
|
1456
|
+
log.info("END step %d: GARBLE hard-stop (%d garble nudges spent) — "
|
|
1457
|
+
"a garbled tool call is never a final answer",
|
|
1458
|
+
step, garble_nudges)
|
|
1459
|
+
self._emit("info", " [turn stopped: repeated malformed tool calls "
|
|
1460
|
+
"— progress note banked; say 'continue' to retry]")
|
|
1461
|
+
return ("[stopped: the model kept emitting malformed tool calls "
|
|
1462
|
+
"— say 'continue' to resume]")
|
|
1390
1463
|
# Iter-3 did-nothing gate: in auto/headless mode (every benchmark run), a
|
|
1391
1464
|
# turn that ends having executed ZERO real tools is never a legitimate
|
|
1392
1465
|
# completion — the keyword intent classifier misses tasks like "extract the
|
|
@@ -1405,7 +1478,9 @@ class Agent:
|
|
|
1405
1478
|
# and end as a hard stop, so --auto-continue (headless) or the
|
|
1406
1479
|
# user's 'continue' (TUI) relaunches a fresh attempt with the
|
|
1407
1480
|
# note instead of silently shipping nothing.
|
|
1408
|
-
self.budget_note = guardrails.progress_note(
|
|
1481
|
+
self.budget_note = guardrails.progress_note(
|
|
1482
|
+
self.messages,
|
|
1483
|
+
rejected_claim=strip_think(text).strip())
|
|
1409
1484
|
log.info("END step %d: FINAL ANSWER blocked by no-empty-diff gate "
|
|
1410
1485
|
"(made_edit=%s, unverified_edit=%s) — progress note banked",
|
|
1411
1486
|
step, made_edit, unverified_edit)
|
|
@@ -1433,6 +1508,8 @@ class Agent:
|
|
|
1433
1508
|
})
|
|
1434
1509
|
if audit:
|
|
1435
1510
|
done_audit_fired = True
|
|
1511
|
+
done_audit_bounces += 1
|
|
1512
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1436
1513
|
_runway = ((self._turn_budget_s
|
|
1437
1514
|
- (time.monotonic() - turn_start))
|
|
1438
1515
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1442,6 +1519,26 @@ class Agent:
|
|
|
1442
1519
|
self.messages.append({"role": "tool", "name": "edit",
|
|
1443
1520
|
"content": audit})
|
|
1444
1521
|
continue
|
|
1522
|
+
# Absent-path re-bounce: the first audit stat'ed task-named paths as
|
|
1523
|
+
# ABSENT and promised acceptance anyway; if they are STILL absent with
|
|
1524
|
+
# runway to spare, bounce one final time (cap 2/turn) — otherwise a
|
|
1525
|
+
# task-named deliverable the audit already flagged absent can go
|
|
1526
|
+
# unwritten and the accept still goes through.
|
|
1527
|
+
if self.mode != "plan" and not self._subagent \
|
|
1528
|
+
and done_audit_fired and done_audit_bounces < 2 \
|
|
1529
|
+
and audit_absent_list:
|
|
1530
|
+
_runway = ((self._turn_budget_s
|
|
1531
|
+
- (time.monotonic() - turn_start))
|
|
1532
|
+
if self._turn_budget_s else None)
|
|
1533
|
+
rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
|
|
1534
|
+
if rebounce:
|
|
1535
|
+
done_audit_bounces += 1
|
|
1536
|
+
audit_absent_list = []
|
|
1537
|
+
log.info("DONE-AUDIT rebounce (final-answer): still-absent "
|
|
1538
|
+
"task paths, runway=%s", _runway)
|
|
1539
|
+
self.messages.append({"role": "tool", "name": "edit",
|
|
1540
|
+
"content": rebounce})
|
|
1541
|
+
continue
|
|
1445
1542
|
log.info("END step %d: model produced a FINAL ANSWER, no tool calls "
|
|
1446
1543
|
"(did_work=%s, made_edit=%s, unverified_edit=%s)",
|
|
1447
1544
|
step, did_work, made_edit, unverified_edit)
|
|
@@ -1451,6 +1548,8 @@ class Agent:
|
|
|
1451
1548
|
# The step acted — the spiral broke on its own, so clear the no-think escalation
|
|
1452
1549
|
# streak: escalation is only for consecutive capped/salvaged stalls.
|
|
1453
1550
|
capped_stall_streak = 0
|
|
1551
|
+
consecutive_garbles = 0 # a parsed call ends the garble streak
|
|
1552
|
+
last_garble_idx = None
|
|
1454
1553
|
|
|
1455
1554
|
# Terminal tool -> end the turn cleanly, but enforce verify-before-done
|
|
1456
1555
|
# (forge's prerequisite idea): if files were changed and nothing has been
|
|
@@ -1497,7 +1596,9 @@ class Agent:
|
|
|
1497
1596
|
# nudges ran out) becomes a resumable hard stop, not a success
|
|
1498
1597
|
# (matplotlib-25332 r3: done accepted at 84s with edits in tree
|
|
1499
1598
|
# and zero successful post-edit commands).
|
|
1500
|
-
self.budget_note = guardrails.progress_note(
|
|
1599
|
+
self.budget_note = guardrails.progress_note(
|
|
1600
|
+
self.messages,
|
|
1601
|
+
rejected_claim=str(terminal.get("summary") or ""))
|
|
1501
1602
|
log.info("END step %d: DONE blocked by no-empty-diff gate "
|
|
1502
1603
|
"(made_edit=%s, unverified_edit=%s) — progress note banked",
|
|
1503
1604
|
step, made_edit, unverified_edit)
|
|
@@ -1540,6 +1641,8 @@ class Agent:
|
|
|
1540
1641
|
})
|
|
1541
1642
|
if audit:
|
|
1542
1643
|
done_audit_fired = True
|
|
1644
|
+
done_audit_bounces += 1
|
|
1645
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1543
1646
|
_runway = ((self._turn_budget_s
|
|
1544
1647
|
- (time.monotonic() - turn_start))
|
|
1545
1648
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1548,6 +1651,22 @@ class Agent:
|
|
|
1548
1651
|
self.messages.append({"role": "tool", "name": "done",
|
|
1549
1652
|
"content": audit})
|
|
1550
1653
|
continue
|
|
1654
|
+
# Absent-path re-bounce (see the final-answer twin above): a
|
|
1655
|
+
# still-absent task-named path at accept time gets ONE more bounce,
|
|
1656
|
+
# then the next done is accepted unconditionally.
|
|
1657
|
+
if done_audit_fired and done_audit_bounces < 2 and audit_absent_list:
|
|
1658
|
+
_runway = ((self._turn_budget_s
|
|
1659
|
+
- (time.monotonic() - turn_start))
|
|
1660
|
+
if self._turn_budget_s else None)
|
|
1661
|
+
rebounce = guardrails.audit_rebounce(audit_absent_list, _runway)
|
|
1662
|
+
if rebounce:
|
|
1663
|
+
done_audit_bounces += 1
|
|
1664
|
+
audit_absent_list = []
|
|
1665
|
+
log.info("DONE-AUDIT rebounce: still-absent task paths, "
|
|
1666
|
+
"runway=%s", _runway)
|
|
1667
|
+
self.messages.append({"role": "tool", "name": "done",
|
|
1668
|
+
"content": rebounce})
|
|
1669
|
+
continue
|
|
1551
1670
|
# Iter-3 deliverable recheck (levers.done_spec_recheck): one last self-check
|
|
1552
1671
|
# that the required outputs actually exist at the right path/format before
|
|
1553
1672
|
# we accept done — the hidden container-end-state verifier gives no second
|
|
@@ -1820,7 +1939,18 @@ class Agent:
|
|
|
1820
1939
|
if made_edit and not _gov_prev_made:
|
|
1821
1940
|
readonly_streak = 0
|
|
1822
1941
|
elif did_work and not made_edit:
|
|
1823
|
-
|
|
1942
|
+
# A bash step that isn't provably read-only is ACTION, not
|
|
1943
|
+
# investigation — `git merge`, `apt-get install`, redirects are ops
|
|
1944
|
+
# progress. Otherwise the gate can count an entire git/ops workflow as
|
|
1945
|
+
# "investigation" and demand an edit at a decision point where there is
|
|
1946
|
+
# nothing to edit yet.
|
|
1947
|
+
if levers.enabled("gate_ops_exempt") and any(
|
|
1948
|
+
n == "bash"
|
|
1949
|
+
and not guardrails.is_readonly_bash(str(a.get("command", "")))
|
|
1950
|
+
for n, a in calls):
|
|
1951
|
+
readonly_streak = 0
|
|
1952
|
+
else:
|
|
1953
|
+
readonly_streak += 1
|
|
1824
1954
|
if not read_only_intent:
|
|
1825
1955
|
gate = guardrails.investigation_gate(readonly_streak, made_edit, gate_nudges)
|
|
1826
1956
|
if gate:
|
|
@@ -637,9 +637,24 @@ def main():
|
|
|
637
637
|
# ships as an empty diff (the NIGHT-7 bail signature).
|
|
638
638
|
continues = args.auto_continue if args.auto_continue is not None \
|
|
639
639
|
else (2 if run_mode == "auto" else 0)
|
|
640
|
-
|
|
640
|
+
used_continues = 0
|
|
641
|
+
while agent.budget_note:
|
|
642
|
+
# Base allowance first; past it, keep granting fresh attempts while most
|
|
643
|
+
# of the task wall is still unspent (bounded by AUTO_CONTINUE_TOTAL_CAP) —
|
|
644
|
+
# the fixed base is wall-blind: build-pov-ray (TB2.1 v1.0.0) gave up after
|
|
645
|
+
# 3 step-capped turns with 94.7% of a 12000s budget unused (plan 107 F3).
|
|
646
|
+
if continues > 0:
|
|
647
|
+
continues -= 1
|
|
648
|
+
elif (args.auto_continue is None and args.turn_budget_s
|
|
649
|
+
and guardrails.replenish_continue(
|
|
650
|
+
args.turn_budget_s, time.monotonic() - task_start,
|
|
651
|
+
used_continues)):
|
|
652
|
+
sys.stderr.write("[governor] wall budget mostly unspent — granting an "
|
|
653
|
+
"extra continue\n")
|
|
654
|
+
else:
|
|
655
|
+
break
|
|
656
|
+
used_continues += 1
|
|
641
657
|
note = agent.budget_note
|
|
642
|
-
continues -= 1
|
|
643
658
|
# The wall budget is a TASK-level deadline (the harness SIGKILLs the whole chad
|
|
644
659
|
# process), not a per-turn one: a relaunch must inherit only the wall time that
|
|
645
660
|
# REMAINS, or the governor / wrap-up / hard-abort windows (measured from each
|