chad-code 1.0.2__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.2/src/chad_code.egg-info → chad_code-1.0.3}/PKG-INFO +15 -15
- {chad_code-1.0.2 → chad_code-1.0.3}/README.md +14 -14
- {chad_code-1.0.2 → chad_code-1.0.3}/pyproject.toml +1 -1
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/agent.py +100 -4
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/guardrails.py +169 -35
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/levers.py +32 -0
- {chad_code-1.0.2 → chad_code-1.0.3/src/chad_code.egg-info}/PKG-INFO +15 -15
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad_code.egg-info/SOURCES.txt +1 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_agent_guards.py +81 -4
- {chad_code-1.0.2 → 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.2 → chad_code-1.0.3}/tests/test_lever_bite.py +53 -6
- {chad_code-1.0.2 → chad_code-1.0.3}/LICENSE +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/setup.cfg +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/__init__.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/atif.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/base_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/bench.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/cli.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/compaction.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/completion_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/config.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/diag.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/ignore.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/lsp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/mcp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/mcp_oauth.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/profiles.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/prompt.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/prove.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/render.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/repomap.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/session.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/skills.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/symbols.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/syntaxgate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/toolcall_parse.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/tools.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/tui.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad/validate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_agent.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_agent_e2e.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_atif.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_bench.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_cli.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_compact_notice.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_compaction.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_completion_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_config.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_confirm_preview.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_drift_warn.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_edit.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_edit_corruption.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_engine.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_engine_kvquant.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_feel_pack.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_gate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_ignore.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_intent.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_levers.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_log_redaction.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_lsp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_mcp.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_mcp_oauth.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_plan_review.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_prove.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_render.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_replace_lines.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_repomap.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_session.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_skills.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_subagent.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_symbols.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_syntaxgate.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_toolcall_parse.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_tools.py +0 -0
- {chad_code-1.0.2 → chad_code-1.0.3}/tests/test_tui.py +0 -0
- {chad_code-1.0.2 → 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
|
|
@@ -1400,12 +1407,33 @@ class Agent:
|
|
|
1400
1407
|
_stripped_for_markers = strip_think(text)
|
|
1401
1408
|
garbled = ("<tool_call>" in _stripped_for_markers
|
|
1402
1409
|
or "</function>" in _stripped_for_markers
|
|
1403
|
-
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
|
|
1404
1428
|
kind, nudge = guardrails.nudge_for_no_calls(
|
|
1405
1429
|
text, hit_cap, made_edit, unverified_edit, read_only_intent,
|
|
1406
1430
|
action_task or run_task, truncation_nudges, answer_nudges,
|
|
1407
|
-
verify_nudges, _has_open_tool_call(text), garbled_call=garbled
|
|
1408
|
-
|
|
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":
|
|
1409
1437
|
truncation_nudges += 1
|
|
1410
1438
|
elif kind == "no-edit":
|
|
1411
1439
|
answer_nudges += 1
|
|
@@ -1417,6 +1445,21 @@ class Agent:
|
|
|
1417
1445
|
has_code, action_task, run_task)
|
|
1418
1446
|
self.messages.append({"role": "tool", "name": "edit", "content": nudge})
|
|
1419
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]")
|
|
1420
1463
|
# Iter-3 did-nothing gate: in auto/headless mode (every benchmark run), a
|
|
1421
1464
|
# turn that ends having executed ZERO real tools is never a legitimate
|
|
1422
1465
|
# completion — the keyword intent classifier misses tasks like "extract the
|
|
@@ -1465,6 +1508,8 @@ class Agent:
|
|
|
1465
1508
|
})
|
|
1466
1509
|
if audit:
|
|
1467
1510
|
done_audit_fired = True
|
|
1511
|
+
done_audit_bounces += 1
|
|
1512
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1468
1513
|
_runway = ((self._turn_budget_s
|
|
1469
1514
|
- (time.monotonic() - turn_start))
|
|
1470
1515
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1474,6 +1519,26 @@ class Agent:
|
|
|
1474
1519
|
self.messages.append({"role": "tool", "name": "edit",
|
|
1475
1520
|
"content": audit})
|
|
1476
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
|
|
1477
1542
|
log.info("END step %d: model produced a FINAL ANSWER, no tool calls "
|
|
1478
1543
|
"(did_work=%s, made_edit=%s, unverified_edit=%s)",
|
|
1479
1544
|
step, did_work, made_edit, unverified_edit)
|
|
@@ -1483,6 +1548,8 @@ class Agent:
|
|
|
1483
1548
|
# The step acted — the spiral broke on its own, so clear the no-think escalation
|
|
1484
1549
|
# streak: escalation is only for consecutive capped/salvaged stalls.
|
|
1485
1550
|
capped_stall_streak = 0
|
|
1551
|
+
consecutive_garbles = 0 # a parsed call ends the garble streak
|
|
1552
|
+
last_garble_idx = None
|
|
1486
1553
|
|
|
1487
1554
|
# Terminal tool -> end the turn cleanly, but enforce verify-before-done
|
|
1488
1555
|
# (forge's prerequisite idea): if files were changed and nothing has been
|
|
@@ -1574,6 +1641,8 @@ class Agent:
|
|
|
1574
1641
|
})
|
|
1575
1642
|
if audit:
|
|
1576
1643
|
done_audit_fired = True
|
|
1644
|
+
done_audit_bounces += 1
|
|
1645
|
+
audit_absent_list = guardrails.audit_absent_paths(audit_task)
|
|
1577
1646
|
_runway = ((self._turn_budget_s
|
|
1578
1647
|
- (time.monotonic() - turn_start))
|
|
1579
1648
|
if self._turn_budget_s else float("inf"))
|
|
@@ -1582,6 +1651,22 @@ class Agent:
|
|
|
1582
1651
|
self.messages.append({"role": "tool", "name": "done",
|
|
1583
1652
|
"content": audit})
|
|
1584
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
|
|
1585
1670
|
# Iter-3 deliverable recheck (levers.done_spec_recheck): one last self-check
|
|
1586
1671
|
# that the required outputs actually exist at the right path/format before
|
|
1587
1672
|
# we accept done — the hidden container-end-state verifier gives no second
|
|
@@ -1854,7 +1939,18 @@ class Agent:
|
|
|
1854
1939
|
if made_edit and not _gov_prev_made:
|
|
1855
1940
|
readonly_streak = 0
|
|
1856
1941
|
elif did_work and not made_edit:
|
|
1857
|
-
|
|
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
|
|
1858
1954
|
if not read_only_intent:
|
|
1859
1955
|
gate = guardrails.investigation_gate(readonly_streak, made_edit, gate_nudges)
|
|
1860
1956
|
if gate:
|
|
@@ -169,6 +169,96 @@ def reverts_working_tree(command: str) -> bool:
|
|
|
169
169
|
return bool(command) and any(p.search(command) for p in _REVERT_PATTERNS)
|
|
170
170
|
|
|
171
171
|
|
|
172
|
+
# Command heads that only OBSERVE state. Used by the investigation gate: a bash step
|
|
173
|
+
# whose every segment starts with one of these (and redirects nothing to a file) is
|
|
174
|
+
# investigation; anything else — `git merge`, `apt-get install`, `mkdir`, `tar x`,
|
|
175
|
+
# a redirect — is ACTION and must reset the read-only streak (otherwise the gate can
|
|
176
|
+
# count an entire git/ops workflow as "investigation" and demand an edit at a decision
|
|
177
|
+
# point where there is nothing to edit yet).
|
|
178
|
+
_READONLY_HEADS = frozenset((
|
|
179
|
+
"ls", "cat", "head", "tail", "less", "more", "grep", "egrep", "fgrep", "rg",
|
|
180
|
+
"find", "file", "stat", "wc", "which", "type", "pwd", "echo", "printf", "du",
|
|
181
|
+
"df", "ps", "env", "printenv", "sort", "uniq", "cut", "tr", "diff", "cmp",
|
|
182
|
+
"md5sum", "sha1sum", "sha256sum", "strings", "xxd", "hexdump", "od",
|
|
183
|
+
"readlink", "realpath", "basename", "dirname", "test", "[", "true", "false",
|
|
184
|
+
"date", "whoami", "id", "uname", "hostname", "tree", "awk", "sed", "jq",
|
|
185
|
+
"column", "nl", "tac", "sleep",
|
|
186
|
+
))
|
|
187
|
+
_READONLY_GIT_SUBS = frozenset((
|
|
188
|
+
"log", "status", "diff", "show", "describe", "rev-parse", "ls-files",
|
|
189
|
+
"ls-remote", "ls-tree", "blame", "reflog", "shortlog", "grep", "cat-file",
|
|
190
|
+
"rev-list", "name-rev", "var", "count-objects",
|
|
191
|
+
))
|
|
192
|
+
# Harmless stderr plumbing stripped before the "any redirect ⇒ mutating" check.
|
|
193
|
+
_STDERR_REDIR_RE = re.compile(r"2>\s*&1|2>\s*/dev/null|&>\s*/dev/null|>\s*/dev/null")
|
|
194
|
+
_SEGMENT_SPLIT_RE = re.compile(r"\|\||&&|[;|]")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def is_readonly_bash(command: str) -> bool:
|
|
198
|
+
"""Conservatively true when a bash command only OBSERVES state: every pipeline
|
|
199
|
+
segment's head is in the read-only allowlist (`cd`/`env`-style prefixes skipped;
|
|
200
|
+
`git <readonly-sub>` allowed; `sed -i` excluded) and nothing is redirected to a
|
|
201
|
+
file. Anything unrecognized is NOT read-only — for the investigation gate that
|
|
202
|
+
is the safe direction (an ops step wrongly counted as investigation harasses the
|
|
203
|
+
model; a read wrongly counted as action merely delays the gate)."""
|
|
204
|
+
if not command.strip():
|
|
205
|
+
return True
|
|
206
|
+
cleaned = _STDERR_REDIR_RE.sub("", command)
|
|
207
|
+
if ">" in cleaned or "<(" in cleaned:
|
|
208
|
+
return False
|
|
209
|
+
for seg in _SEGMENT_SPLIT_RE.split(cleaned):
|
|
210
|
+
words = seg.strip().split()
|
|
211
|
+
# skip wrappers/prefixes that don't decide the verb
|
|
212
|
+
while words and (words[0] == "cd" or "=" in words[0] or words[0] in
|
|
213
|
+
("env", "sudo", "command", "builtin", "time", "nice")):
|
|
214
|
+
if words[0] == "cd": # `cd x && grep …`: drop `cd` + its argument
|
|
215
|
+
words = words[2:]
|
|
216
|
+
else:
|
|
217
|
+
words = words[1:]
|
|
218
|
+
if not words:
|
|
219
|
+
continue
|
|
220
|
+
head = words[0]
|
|
221
|
+
if head == "git":
|
|
222
|
+
if len(words) < 2 or words[1] not in _READONLY_GIT_SUBS:
|
|
223
|
+
return False
|
|
224
|
+
elif head not in _READONLY_HEADS:
|
|
225
|
+
return False
|
|
226
|
+
elif head == "sed" and any(w.startswith("-i") or w.startswith("--in-place")
|
|
227
|
+
for w in words[1:]):
|
|
228
|
+
return False
|
|
229
|
+
return True
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def audit_absent_paths(task_text, paths=None):
|
|
233
|
+
"""The task-mentioned paths that do not currently exist — recorded at the first
|
|
234
|
+
done-audit bounce so `audit_rebounce` can later check exactly these (and nothing
|
|
235
|
+
else) at accept time."""
|
|
236
|
+
if paths is None:
|
|
237
|
+
paths = audit_extract_paths(task_text)
|
|
238
|
+
return [p for p in paths if not os.path.exists(p)]
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def audit_rebounce(absent_paths, runway_s):
|
|
242
|
+
"""The one final done-audit bounce (levers.audit_absent_rebounce), or None to
|
|
243
|
+
accept. Fires only when a path the FIRST audit already reported absent is STILL
|
|
244
|
+
absent and there is real runway left — the miss it closes is a task-named
|
|
245
|
+
deliverable the audit stat'ed as absent, where the acceptance promise forced the
|
|
246
|
+
done through and the file was never written. Caller owns the 2-bounce cap and the
|
|
247
|
+
still-once-per-turn latch semantics."""
|
|
248
|
+
if not levers.enabled("audit_absent_rebounce") or not absent_paths:
|
|
249
|
+
return None
|
|
250
|
+
if runway_s is not None and runway_s <= AUDIT_MIN_RUNWAY_S:
|
|
251
|
+
return None
|
|
252
|
+
still = [p for p in absent_paths if not os.path.exists(p)]
|
|
253
|
+
if not still:
|
|
254
|
+
return None
|
|
255
|
+
return ("[final audit — the task statement names path(s) that still do not "
|
|
256
|
+
"exist:\n" + "\n".join(f" - {p}" for p in still) +
|
|
257
|
+
"\nIf the task requires them, create them at exactly these paths now "
|
|
258
|
+
"(then re-verify). If the task genuinely wants them absent, call done "
|
|
259
|
+
"again. Your next `done` will be accepted unconditionally.]")
|
|
260
|
+
|
|
261
|
+
|
|
172
262
|
def update_work_flags(name, args, result, did_work, made_edit, unverified_edit):
|
|
173
263
|
"""Update the (did_work, made_edit, unverified_edit) guardrail flags after one
|
|
174
264
|
tool result; returns the new triple. A substantive tool counts as real work; a
|
|
@@ -416,8 +506,17 @@ def done_audit(task_text, turn_state):
|
|
|
416
506
|
req_lines = audit_requirement_lines(task_text, paths)
|
|
417
507
|
if not req_lines:
|
|
418
508
|
return None
|
|
419
|
-
|
|
420
|
-
|
|
509
|
+
# The acceptance promise must stay truthful: with the absent-path re-bounce
|
|
510
|
+
# armed (levers.audit_absent_rebounce) one further existence-only check may run,
|
|
511
|
+
# so say so; otherwise keep the original unconditional promise (the anti-spiral
|
|
512
|
+
# latch depends on the model believing it).
|
|
513
|
+
if levers.enabled("audit_absent_rebounce"):
|
|
514
|
+
promise = ("your NEXT `done` will be accepted after at most one further "
|
|
515
|
+
"check that the paths below exist.")
|
|
516
|
+
else:
|
|
517
|
+
promise = "your NEXT `done` will be accepted without further audit."
|
|
518
|
+
parts = ["[done-audit — one-time check before this `done` is accepted; " + promise
|
|
519
|
+
+ " The hidden grader checks "
|
|
421
520
|
"the task's OWN requirements, not the checks you happened to run. From the "
|
|
422
521
|
"task statement:"]
|
|
423
522
|
parts += [f" > {ln}" for ln in req_lines]
|
|
@@ -431,48 +530,83 @@ def done_audit(task_text, turn_state):
|
|
|
431
530
|
return "\n".join(parts)
|
|
432
531
|
|
|
433
532
|
|
|
533
|
+
GARBLE_NUDGE_CAP = 6 # per-turn garble re-nudges (own counter, separate from truncation)
|
|
534
|
+
|
|
535
|
+
# Placeholder that replaces a garbled assistant message body once the NEXT step
|
|
536
|
+
# garbles too — the wrong dialect must not stay in context as a few-shot example
|
|
537
|
+
# (repeated garbles in a row each condition on the last). Costs one prefix-cache
|
|
538
|
+
# invalidation on a rare path.
|
|
539
|
+
GARBLE_SCRUBBED = "[a malformed tool call was removed here — it did not run]"
|
|
540
|
+
|
|
541
|
+
# Appended to the garble nudge from the 2nd consecutive garbled step: the model is
|
|
542
|
+
# clearly stuck in a wrong dialect, so show the contract instead of describing it.
|
|
543
|
+
TOOLCALL_EXEMPLAR = (
|
|
544
|
+
"\nThis is the ONLY valid format — copy its shape exactly:\n"
|
|
545
|
+
"<tool_call>\n"
|
|
546
|
+
'{"name": "bash", "arguments": {"command": "ls /app"}}\n'
|
|
547
|
+
"</tool_call>\n"
|
|
548
|
+
"No <function=...>, no <parameter=...> tags, nothing else inside the block.")
|
|
549
|
+
|
|
550
|
+
|
|
434
551
|
def nudge_for_no_calls(text, hit_cap, made_edit, unverified_edit, read_only_intent,
|
|
435
552
|
action_task, truncation_nudges, answer_nudges, verify_nudges,
|
|
436
|
-
open_tool_call, garbled_call=False
|
|
553
|
+
open_tool_call, garbled_call=False, garble_nudges=0,
|
|
554
|
+
consecutive_garbles=0):
|
|
437
555
|
"""Pick the nudge for a step that produced NO tool call, in the original priority
|
|
438
|
-
order: (1) TRUNCATED —
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
556
|
+
order: (1) GARBLE/TRUNCATED — a tool-call attempt that parsed to nothing, or the
|
|
557
|
+
token cap hit mid-thought, so it isn't an answer; (2) ANSWERED ON PAPER —
|
|
558
|
+
produced/described code but never applied it; (3) UNVERIFIED EDIT — edited but
|
|
559
|
+
never ran the check. Pure: returns (kind, nudge_text) or (None, None); the caller
|
|
560
|
+
bumps the matching counter and appends the nudge. `kind` is one of 'garble' /
|
|
561
|
+
'truncated' / 'no-edit' / 'unverified-edit'. (open_tool_call is run_turn's
|
|
562
|
+
_has_open_tool_call(text): an unbalanced <tool_call>/<function> that parsed to
|
|
563
|
+
nothing.)"""
|
|
444
564
|
has_code = "```" in text
|
|
445
|
-
#
|
|
446
|
-
#
|
|
447
|
-
#
|
|
448
|
-
#
|
|
449
|
-
#
|
|
450
|
-
#
|
|
451
|
-
#
|
|
452
|
-
|
|
453
|
-
|
|
565
|
+
# A tool-call attempt that parsed to zero calls is never a final answer, whether
|
|
566
|
+
# or not the token cap was hit. Causes: (a) hit_cap — a `write` whose content
|
|
567
|
+
# overran the budget (guide it to write in bounded pieces); (b) an unclosed or
|
|
568
|
+
# closed-but-unparseable block — sampling glitch, premature EOS, or a slide into
|
|
569
|
+
# a foreign XML tool-call dialect. With the garble_never_final lever these use
|
|
570
|
+
# their OWN counter (garble_nudges, cap 6) — a shared `truncation_nudges < 2` lets
|
|
571
|
+
# an unrelated step-0 cap-hit spend the garble budget, after which a run of garbles
|
|
572
|
+
# can be ACCEPTED as the final answer with most of the wall budget still left. This
|
|
573
|
+
# fires before the bare-stall branch so a garbled call isn't misread as an empty
|
|
574
|
+
# stall.
|
|
575
|
+
if open_tool_call and hit_cap:
|
|
576
|
+
# A call cut off AT the token cap is a length problem (a too-long `write`),
|
|
577
|
+
# not a dialect garble — it stays in the truncation family/counter.
|
|
578
|
+
if truncation_nudges < 2:
|
|
454
579
|
nudge = ("[your tool call was cut off at the length limit — the "
|
|
455
580
|
"content was too long to emit in one call. Do NOT retry it "
|
|
456
581
|
"whole. Create the file with `write` using only the FIRST "
|
|
457
582
|
"portion of the content, then append the rest with one or "
|
|
458
583
|
"more `edit` calls. Emit one complete tool call at a time.]")
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
584
|
+
return "truncated", nudge
|
|
585
|
+
elif open_tool_call or garbled_call:
|
|
586
|
+
if levers.enabled("garble_never_final"):
|
|
587
|
+
allowed, kind = garble_nudges < GARBLE_NUDGE_CAP, "garble"
|
|
588
|
+
else: # legacy (ablation arm): shared counter, kind counted as truncation
|
|
589
|
+
allowed, kind = truncation_nudges < 2, "truncated"
|
|
590
|
+
if allowed:
|
|
591
|
+
if open_tool_call:
|
|
592
|
+
nudge = ("[your last tool call was malformed and did not run — it opened a "
|
|
593
|
+
"<tool_call> (or <function=…>) that was never properly closed, so no "
|
|
594
|
+
"tool executed and nothing happened. Re-emit it now as ONE complete, "
|
|
595
|
+
"well-formed <tool_call> block with valid JSON arguments.]")
|
|
596
|
+
else:
|
|
597
|
+
# garbled_call: the block WAS closed but nothing inside parsed — mixed
|
|
598
|
+
# JSON/XML dialects, invalid JSON the repair pass couldn't reconstruct,
|
|
599
|
+
# etc. Without this branch the garble is accepted as a final answer —
|
|
600
|
+
# e.g. a `{"name": "bash", … </parameter></function></tool_call>` mash
|
|
601
|
+
# that ends the task with budget still left.
|
|
602
|
+
nudge = ("[your last tool call was malformed and did not run — the "
|
|
603
|
+
"<tool_call> block did not contain one valid JSON object, so no "
|
|
604
|
+
"tool executed and nothing happened. Re-emit it now as ONE "
|
|
605
|
+
"complete <tool_call> block: a single JSON object with \"name\" "
|
|
606
|
+
"and \"arguments\", no XML tags inside.]")
|
|
607
|
+
if kind == "garble" and consecutive_garbles >= 2:
|
|
608
|
+
nudge = nudge[:-1] + TOOLCALL_EXEMPLAR + "]"
|
|
609
|
+
return kind, nudge
|
|
476
610
|
if hit_cap and truncation_nudges < 2:
|
|
477
611
|
# Cap hit but the call (if any) was balanced — a plain mid-thought truncation.
|
|
478
612
|
nudge = ("[your reply was cut off at the length limit before you "
|
|
@@ -277,6 +277,38 @@ LEVERS: dict[str, Lever] = {
|
|
|
277
277
|
"Only active with a wall budget configured; inert in interactive/unmetered runs.",
|
|
278
278
|
"iter12"),
|
|
279
279
|
|
|
280
|
+
# --- iter-13: lever *interactions*. Two otherwise-winnable tasks were lost not
|
|
281
|
+
# to the model but to guardrails fighting each other — a garbled tool
|
|
282
|
+
# call accepted as the final answer once the shared nudge counter and the
|
|
283
|
+
# done-audit latch were both spent, and the investigation gate counting a
|
|
284
|
+
# `git merge`/`apt-get` ops workflow as "investigation" and demanding an edit
|
|
285
|
+
# at a point where there was nothing yet to edit. --------------------------
|
|
286
|
+
"garble_never_final": Lever(
|
|
287
|
+
"A step whose text contains tool-call markers is NEVER accepted as a final "
|
|
288
|
+
"answer and never reaches the done-audit final-answer twin: garbles get their "
|
|
289
|
+
"own nudge counter (cap 6, not shared with cap-truncations), a canonical "
|
|
290
|
+
"call exemplar from the 2nd consecutive garble, a scrub of the previous "
|
|
291
|
+
"garbled message body (so the model stops few-shotting its own broken "
|
|
292
|
+
"dialect), and — if nudges run out — a banked-note hard stop. OFF restores "
|
|
293
|
+
"the shared-counter failure: a run of garbles burns the shared counter + "
|
|
294
|
+
"audit latch and the last is accepted as the answer with most of the wall "
|
|
295
|
+
"budget still left.",
|
|
296
|
+
"iter13", REGRESSION_GUARD),
|
|
297
|
+
"audit_absent_rebounce": Lever(
|
|
298
|
+
"When the done-audit's first bounce stat'ed task-mentioned paths as ABSENT "
|
|
299
|
+
"and they are STILL absent at the accepting done/final-answer (runway > "
|
|
300
|
+
"120s), bounce one final time naming only the missing paths; the next done "
|
|
301
|
+
"is then accepted unconditionally (hard cap 2 bounces/turn). The first "
|
|
302
|
+
"audit's acceptance promise is reworded to stay truthful while this is on.",
|
|
303
|
+
"iter13"),
|
|
304
|
+
"gate_ops_exempt": Lever(
|
|
305
|
+
"A bash step whose command is not provably read-only counts as ACTION for "
|
|
306
|
+
"the investigation gate (resets the read-only streak): `git merge`, "
|
|
307
|
+
"`apt-get install`, redirects etc. are ops progress, not investigation. "
|
|
308
|
+
"The gate keeps firing on genuine grep/read loops. OFF restores the gate "
|
|
309
|
+
"harassing ops-heavy tasks that legitimately run many non-read commands.",
|
|
310
|
+
"iter13"),
|
|
311
|
+
|
|
280
312
|
# --- from the LangChain harness-tuning playbook. -------------------------------
|
|
281
313
|
"compact_notice": Lever(
|
|
282
314
|
"After compaction, inject an in-band message telling the model its context was "
|