chad-code 1.0.1__tar.gz → 1.0.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {chad_code-1.0.1/src/chad_code.egg-info → chad_code-1.0.2}/PKG-INFO +1 -1
- {chad_code-1.0.1 → chad_code-1.0.2}/pyproject.toml +1 -1
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/__init__.py +1 -1
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/agent.py +51 -17
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/cli.py +17 -2
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/guardrails.py +87 -8
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/levers.py +8 -6
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/prompt.py +15 -1
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/syntaxgate.py +25 -4
- {chad_code-1.0.1 → chad_code-1.0.2/src/chad_code.egg-info}/PKG-INFO +1 -1
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_agent_e2e.py +40 -8
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_agent_guards.py +84 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_intent.py +27 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_syntaxgate.py +25 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/LICENSE +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/README.md +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/setup.cfg +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/atif.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/base_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/bench.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/compaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/completion_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/config.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/diag.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/ignore.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/lsp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/mcp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/mcp_oauth.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/mlx_fastpath.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/mlx_qsdpa.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/profiles.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/prove.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/render.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/repomap.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/session.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/skills.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/symbols.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/toolcall_parse.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/tools.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/tui.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad/validate.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad_code.egg-info/SOURCES.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad_code.egg-info/dependency_links.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad_code.egg-info/entry_points.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad_code.egg-info/requires.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/src/chad_code.egg-info/top_level.txt +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_agent.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_atif.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_bench.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_cli.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_compact_notice.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_compaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_completion_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_config.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_confirm_preview.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_done_audit.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_drift_warn.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_edit.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_edit_corruption.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_engine.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_engine_kvquant.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_feel_pack.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_gate.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_ignore.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_lever_bite.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_levers.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_log_redaction.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_lsp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_mcp.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_mcp_oauth.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_mlx_fastpath.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_mlx_qsdpa.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_plan_review.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_prove.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_render.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_replace_lines.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_repomap.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_session.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_skills.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_subagent.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_symbols.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_toolcall_parse.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_tools.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_tui.py +0 -0
- {chad_code-1.0.1 → chad_code-1.0.2}/tests/test_validate.py +0 -0
|
@@ -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.2"
|
|
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"
|
|
@@ -807,12 +807,18 @@ class Agent:
|
|
|
807
807
|
# Turn-level cumulative think budget: distinct from self.think_tokens
|
|
808
808
|
# (lifetime across the whole Agent instance, used for cross-turn telemetry) — this
|
|
809
809
|
# 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
|
-
#
|
|
810
|
+
# decode-speed-aware budget. turn_think_exhausted latches the once-only
|
|
811
|
+
# log/steer; past it, no-think steps are paid on a duty cycle
|
|
812
|
+
# (guardrails.turn_think_throttle) rather than muting the rest of the turn —
|
|
813
|
+
# the blanket mute regressed run1 passes with garbled no-think tails (plan 107).
|
|
814
|
+
# landing_no_think is the 103 landing's own unconditional latch: once the hard
|
|
815
|
+
# wrap-up fires, the landing and everything after it stay no-think regardless.
|
|
813
816
|
turn_think_tokens = 0
|
|
814
817
|
turn_think_half_fired = False
|
|
815
818
|
turn_think_exhausted = False
|
|
819
|
+
turn_think_nothink_paid = 0
|
|
820
|
+
turn_think_budget_now = 0
|
|
821
|
+
landing_no_think = False
|
|
816
822
|
# Runaway-turn governor state. turn_start drives the optional wall
|
|
817
823
|
# budget; gov_band is the highest budget checkpoint already evaluated; gov_progress
|
|
818
824
|
# tracks whether a change landed+verified within the CURRENT band (resets each time
|
|
@@ -845,6 +851,12 @@ class Agent:
|
|
|
845
851
|
# where it also selects the plan-mode prefix.)
|
|
846
852
|
action_task = self.mode != "plan" and _intent["action"]
|
|
847
853
|
read_only_intent = _intent["read_only"]
|
|
854
|
+
# Run-task intent (start/boot/serve/… — system-state imperatives with no file
|
|
855
|
+
# deliverable): arms the anti-bail nudges alongside action_task but is kept OUT
|
|
856
|
+
# of the no-empty-diff done gates below, which demand a landed edit a run task
|
|
857
|
+
# legitimately never makes (plan 107 follow-up: qemu-startup classified as
|
|
858
|
+
# neither, so a prose give-up with 81% of the wall left took the weakest path).
|
|
859
|
+
run_task = self.mode != "plan" and _intent.get("run", False)
|
|
848
860
|
# Progress-aware step cap (see guardrails.extend_step_cap): max_steps is the
|
|
849
861
|
# WINDOW size, not a hard kill. A window that landed+verified a change earns
|
|
850
862
|
# another window (warm cache — no re-prefill, unlike a governor rollover); a
|
|
@@ -963,9 +975,19 @@ class Agent:
|
|
|
963
975
|
# <think> off so the model must act, then the flag clears and thinking restores.
|
|
964
976
|
# Consumes the one-shot here; the render/decode/accounting below all key off
|
|
965
977
|
# `step_thinking` rather than self.thinking for exactly this step.
|
|
966
|
-
# Turn-level think budget:
|
|
967
|
-
#
|
|
968
|
-
|
|
978
|
+
# Turn-level think budget: past exhaustion, forced no-think steps are
|
|
979
|
+
# paid on a duty cycle (one per TURN_THINK_REARM_TOK further think tokens)
|
|
980
|
+
# so thinking RESTORES once the model stops over-spending — a blanket
|
|
981
|
+
# rest-of-turn mute regressed run1 passes (plan 107 F1). The 103 landing's
|
|
982
|
+
# landing_no_think stays unconditional.
|
|
983
|
+
_tt_throttled = (turn_think_exhausted
|
|
984
|
+
and guardrails.turn_think_throttle(
|
|
985
|
+
turn_think_tokens, turn_think_budget_now,
|
|
986
|
+
turn_think_nothink_paid))
|
|
987
|
+
if _tt_throttled:
|
|
988
|
+
turn_think_nothink_paid += 1
|
|
989
|
+
step_thinking = (self.thinking and not landing_no_think
|
|
990
|
+
and not _tt_throttled)
|
|
969
991
|
if no_think_next:
|
|
970
992
|
no_think_next = False
|
|
971
993
|
capped_stall_streak = 0
|
|
@@ -1203,9 +1225,15 @@ class Agent:
|
|
|
1203
1225
|
# turns (a Q&A turn shouldn't be pushed to "act now") and without a configured
|
|
1204
1226
|
# wall budget (interactive/unmetered runs, like wrapup_window above).
|
|
1205
1227
|
turn_think_tokens += _think_delta
|
|
1206
|
-
|
|
1228
|
+
# Inert below TURN_THINK_MIN_WALL_S: a short auto-continue tail clamps to
|
|
1229
|
+
# the LO budget and half-fires on its first step, churning against
|
|
1230
|
+
# hard_wrapup's landing (plan 107 F2 — the regex-log relaunch signature).
|
|
1231
|
+
if (self._turn_budget_s
|
|
1232
|
+
and self._turn_budget_s >= guardrails.TURN_THINK_MIN_WALL_S
|
|
1233
|
+
and self.mode != "plan" and not read_only_intent
|
|
1207
1234
|
and levers.enabled("turn_think_budget")):
|
|
1208
1235
|
_tt_budget = guardrails.turn_think_budget(self._turn_budget_s, self.tok_per_s)
|
|
1236
|
+
turn_think_budget_now = _tt_budget
|
|
1209
1237
|
_tt_decision, turn_think_half_fired, turn_think_exhausted = (
|
|
1210
1238
|
guardrails.turn_think_budget_check(
|
|
1211
1239
|
turn_think_tokens, _tt_budget,
|
|
@@ -1217,10 +1245,11 @@ class Agent:
|
|
|
1217
1245
|
"content": guardrails.TURN_THINK_BUDGET_STEER})
|
|
1218
1246
|
elif _tt_decision == "exhausted":
|
|
1219
1247
|
log.info("THINK-BUDGET exhausted at step %d: %d/%d cumulative think "
|
|
1220
|
-
"tok this turn —
|
|
1221
|
-
turn_think_tokens,
|
|
1248
|
+
"tok this turn — throttling <think> (one action step per "
|
|
1249
|
+
"%d further think tok)", step, turn_think_tokens,
|
|
1250
|
+
_tt_budget, guardrails.TURN_THINK_REARM_TOK)
|
|
1222
1251
|
self._emit("info", " [reasoning budget exhausted for this turn — "
|
|
1223
|
-
"
|
|
1252
|
+
"throttling further <think>]")
|
|
1224
1253
|
|
|
1225
1254
|
log.info("step %d: %d tok @ %.1f tok/s | prefill %d new + %d cached | "
|
|
1226
1255
|
"accept %.2f", step, stats.generated_tokens, stats.tok_per_s,
|
|
@@ -1277,7 +1306,7 @@ class Agent:
|
|
|
1277
1306
|
# above) with its tokens in the KV cache. Force ONE time-boxed, no-think
|
|
1278
1307
|
# landing turn so the model writes its best artifacts to the exact paths before
|
|
1279
1308
|
# the harness kills the turn at the wall — instead of dying mid-token with
|
|
1280
|
-
# nothing landed.
|
|
1309
|
+
# nothing landed. landing_no_think makes the landing (and any step after it)
|
|
1281
1310
|
# no-think so it can't re-open a spiral; the latch disarms _deadline_stop, so
|
|
1282
1311
|
# the landing generation runs to its token box, not another abort. deadline_fired
|
|
1283
1312
|
# is exclusive with rep_fired / the think-cap below (it is checked first in
|
|
@@ -1286,7 +1315,7 @@ class Agent:
|
|
|
1286
1315
|
# there is rarely one, and partial-tool-call surgery is not worth its risk here.
|
|
1287
1316
|
if deadline_fired[0] and not hard_wrapup_fired:
|
|
1288
1317
|
hard_wrapup_fired = True
|
|
1289
|
-
|
|
1318
|
+
landing_no_think = True
|
|
1290
1319
|
_remaining = self._turn_budget_s - (time.monotonic() - turn_start)
|
|
1291
1320
|
log.info("HARD-WRAPUP abort at step %d: %.0fs left, gen was %d tok",
|
|
1292
1321
|
step, _remaining, stats.generated_tokens)
|
|
@@ -1374,8 +1403,8 @@ class Agent:
|
|
|
1374
1403
|
or "<function=" in _stripped_for_markers)
|
|
1375
1404
|
kind, nudge = guardrails.nudge_for_no_calls(
|
|
1376
1405
|
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)
|
|
1406
|
+
action_task or run_task, truncation_nudges, answer_nudges,
|
|
1407
|
+
verify_nudges, _has_open_tool_call(text), garbled_call=garbled)
|
|
1379
1408
|
if kind == "truncated":
|
|
1380
1409
|
truncation_nudges += 1
|
|
1381
1410
|
elif kind == "no-edit":
|
|
@@ -1384,7 +1413,8 @@ class Agent:
|
|
|
1384
1413
|
verify_nudges += 1
|
|
1385
1414
|
if nudge:
|
|
1386
1415
|
log.info("END-ANSWER rejected step %d: %s (hit_cap=%s, has_code=%s, "
|
|
1387
|
-
"action_task=%s)", step, kind, hit_cap,
|
|
1416
|
+
"action_task=%s, run_task=%s)", step, kind, hit_cap,
|
|
1417
|
+
has_code, action_task, run_task)
|
|
1388
1418
|
self.messages.append({"role": "tool", "name": "edit", "content": nudge})
|
|
1389
1419
|
continue
|
|
1390
1420
|
# Iter-3 did-nothing gate: in auto/headless mode (every benchmark run), a
|
|
@@ -1405,7 +1435,9 @@ class Agent:
|
|
|
1405
1435
|
# and end as a hard stop, so --auto-continue (headless) or the
|
|
1406
1436
|
# user's 'continue' (TUI) relaunches a fresh attempt with the
|
|
1407
1437
|
# note instead of silently shipping nothing.
|
|
1408
|
-
self.budget_note = guardrails.progress_note(
|
|
1438
|
+
self.budget_note = guardrails.progress_note(
|
|
1439
|
+
self.messages,
|
|
1440
|
+
rejected_claim=strip_think(text).strip())
|
|
1409
1441
|
log.info("END step %d: FINAL ANSWER blocked by no-empty-diff gate "
|
|
1410
1442
|
"(made_edit=%s, unverified_edit=%s) — progress note banked",
|
|
1411
1443
|
step, made_edit, unverified_edit)
|
|
@@ -1497,7 +1529,9 @@ class Agent:
|
|
|
1497
1529
|
# nudges ran out) becomes a resumable hard stop, not a success
|
|
1498
1530
|
# (matplotlib-25332 r3: done accepted at 84s with edits in tree
|
|
1499
1531
|
# and zero successful post-edit commands).
|
|
1500
|
-
self.budget_note = guardrails.progress_note(
|
|
1532
|
+
self.budget_note = guardrails.progress_note(
|
|
1533
|
+
self.messages,
|
|
1534
|
+
rejected_claim=str(terminal.get("summary") or ""))
|
|
1501
1535
|
log.info("END step %d: DONE blocked by no-empty-diff gate "
|
|
1502
1536
|
"(made_edit=%s, unverified_edit=%s) — progress note banked",
|
|
1503
1537
|
step, made_edit, unverified_edit)
|
|
@@ -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
|
|
@@ -924,7 +924,21 @@ PROGRESS_NOTE_HEADER = (
|
|
|
924
924
|
"Progress so far (auto-summarized — the previous attempt ran out of budget):")
|
|
925
925
|
|
|
926
926
|
|
|
927
|
-
|
|
927
|
+
# Matches a hypothesis that ASSERTS the work is already finished (the poison a
|
|
928
|
+
# rejected-done relaunch must not inherit as its leading "fact"), NOT a diagnosis that
|
|
929
|
+
# merely contains a success-ish word. Every completion word is anchored to a subject or
|
|
930
|
+
# to "already": a bare `build\s+is\s` matched "reBUILD IS failing", and a standalone
|
|
931
|
+
# `verified`/`successful` matched "SUCCESSFULLY reproduced the crash" — both real
|
|
932
|
+
# diagnoses, dropped on the exact path meant to preserve them (eng-review outside-voice
|
|
933
|
+
# F1). \b on the subjects also stops "build" from matching inside "rebuild".
|
|
934
|
+
_COMPLETION_CLAIM_RE = re.compile(
|
|
935
|
+
r"(already\s+(?:built|complete|done|installed|present|correct|verified|passe?[sd])"
|
|
936
|
+
r"|\b(?:build|task|project|everything|it)\s+is\s+"
|
|
937
|
+
r"(?:complete|done|built|ready|correct|passing)"
|
|
938
|
+
r"|task\s+is\s+(?:done|complete))", re.IGNORECASE)
|
|
939
|
+
|
|
940
|
+
|
|
941
|
+
def progress_note(messages, max_lines: int = 24, rejected_claim: str | None = None) -> str:
|
|
928
942
|
"""Synthesize a ≤`max_lines` progress note from the transcript with NO model call, so
|
|
929
943
|
a hard-stopped turn can seed a fresh relaunch (sheds the ramble AND the huge prefill
|
|
930
944
|
the stuck model was dragging around). Deterministic: pulls the model's last working
|
|
@@ -933,7 +947,15 @@ def progress_note(messages, max_lines: int = 24) -> str:
|
|
|
933
947
|
facts the executor CANNOT reconstruct from a clean context — the diagnosis it reached,
|
|
934
948
|
what it already tried, and what failed last. The old note carried only file names, so
|
|
935
949
|
a relaunch re-derived the whole investigation and re-spent the budget it was meant to
|
|
936
|
-
save (django-14007/-14404: the correct fix was stated in prose, then lost).
|
|
950
|
+
save (django-14007/-14404: the correct fix was stated in prose, then lost).
|
|
951
|
+
|
|
952
|
+
`rejected_claim` (plan 107 follow-up — the build-pov-ray poisoning loop): when the
|
|
953
|
+
turn ended via a REJECTED completion claim (done / final answer blocked by the
|
|
954
|
+
no-empty-diff gate), pass the claim text. The note then (a) leads with an explicit
|
|
955
|
+
warning that the claim was rejected, and (b) drops a working hypothesis that itself
|
|
956
|
+
asserts completion — otherwise every fresh relaunch inherits "the build is already
|
|
957
|
+
complete and verified" as its leading fact, re-confirms it, and re-dones (observed
|
|
958
|
+
6 relaunches in a row, task 0/1 with 86% of the wall unused)."""
|
|
937
959
|
from .toolcall_parse import parse_tool_calls
|
|
938
960
|
edited, commands, examined = [], [], []
|
|
939
961
|
last_error = None
|
|
@@ -968,6 +990,17 @@ def progress_note(messages, max_lines: int = 24) -> str:
|
|
|
968
990
|
# lever adds, and therefore what its delta measures.
|
|
969
991
|
rich = levers.enabled("progress_note_rich")
|
|
970
992
|
lines = [PROGRESS_NOTE_HEADER]
|
|
993
|
+
if rejected_claim:
|
|
994
|
+
lines.append(
|
|
995
|
+
"WARNING: the previous attempt ended by CLAIMING the task was complete "
|
|
996
|
+
f"(\"{rejected_claim.strip()[:160]}\") but the claim was REJECTED — it had "
|
|
997
|
+
"landed no verified change. Treat every success statement below as "
|
|
998
|
+
"unverified and likely wrong: re-verify against the task statement with "
|
|
999
|
+
"fresh commands and MAKE the required change before calling done.")
|
|
1000
|
+
# A hypothesis that itself asserts completion is exactly the poison the
|
|
1001
|
+
# warning exists for — never carry it forward as the leading "fact".
|
|
1002
|
+
if hypothesis and _COMPLETION_CLAIM_RE.search(hypothesis):
|
|
1003
|
+
hypothesis = None
|
|
971
1004
|
if rich and hypothesis:
|
|
972
1005
|
# The single most valuable thing to carry: the diagnosis the stuck attempt
|
|
973
1006
|
# reached. Keep the tail (the conclusion), clipped so it can't blow the budget.
|
|
@@ -1088,6 +1121,28 @@ def relaunch_budget(total_wall_s, task_elapsed_s) -> float | None:
|
|
|
1088
1121
|
return remaining
|
|
1089
1122
|
|
|
1090
1123
|
|
|
1124
|
+
AUTO_CONTINUE_TOTAL_CAP = 6 # absolute relaunch ceiling per task (base + extras)
|
|
1125
|
+
AUTO_CONTINUE_REPLENISH_FRAC = 0.5 # grant extras only while this fraction of the task
|
|
1126
|
+
# wall is still unspent
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def replenish_continue(total_wall_s, elapsed_s, used_continues,
|
|
1130
|
+
cap: int = AUTO_CONTINUE_TOTAL_CAP,
|
|
1131
|
+
frac: float = AUTO_CONTINUE_REPLENISH_FRAC) -> bool:
|
|
1132
|
+
"""Grant an auto-continue relaunch beyond the base allowance? The fixed base of 2
|
|
1133
|
+
is wall-blind: build-pov-ray (TB2.1 v1.0.0 run) burned 3 step-capped turns in 637s
|
|
1134
|
+
and gave up with 94.7% of a 12000s budget unused (plan 107 F3). While more than
|
|
1135
|
+
`frac` of the task wall remains unspent, keep granting fresh attempts, bounded by
|
|
1136
|
+
`cap` total relaunches so a pathological fast-stall loop still terminates well
|
|
1137
|
+
before the harness SIGKILL. No wall budget -> never (interactive runs keep the
|
|
1138
|
+
explicit base allowance only). Pure/testable; the caller counts usage."""
|
|
1139
|
+
if not total_wall_s:
|
|
1140
|
+
return False
|
|
1141
|
+
if used_continues >= cap:
|
|
1142
|
+
return False
|
|
1143
|
+
return (total_wall_s - elapsed_s) > frac * total_wall_s
|
|
1144
|
+
|
|
1145
|
+
|
|
1091
1146
|
def hard_wrapup_deadline(wall_budget_s, already_fired, plan_mode, read_only) -> float | None:
|
|
1092
1147
|
"""Should the hard wrap-up abort ARM for this step? Returns the land_margin (seconds
|
|
1093
1148
|
before the wall at which to cut the generation) when it should, else None. Lever-gated
|
|
@@ -1116,6 +1171,12 @@ TURN_THINK_BUDGET_LO = 8000 # floor: a *turn* budget, not a per-generation on
|
|
|
1116
1171
|
# must clear passes' own median spend (10.7k) comfortably
|
|
1117
1172
|
TURN_THINK_BUDGET_HI = 24000 # ceiling: clamp regardless of wall/decode-speed inputs
|
|
1118
1173
|
TURN_THINK_BUDGET_FRAC = 0.35 # the budget's TIME cost should stay <= ~35% of the wall
|
|
1174
|
+
TURN_THINK_MIN_WALL_S = 300.0 # below this wall budget the mechanism is inert: a short
|
|
1175
|
+
# auto-continue tail clamps to LO and half-fires on its
|
|
1176
|
+
# first step, churning against hard_wrapup's landing
|
|
1177
|
+
# (plan 107 F2 — the regex-log relaunch signature)
|
|
1178
|
+
TURN_THINK_REARM_TOK = 3000 # past exhaustion, one forced no-think step is owed per
|
|
1179
|
+
# this many FURTHER think tokens spent (plan 107 F1)
|
|
1119
1180
|
|
|
1120
1181
|
|
|
1121
1182
|
def turn_think_budget(wall_budget_s, decode_tps, frac: float = TURN_THINK_BUDGET_FRAC,
|
|
@@ -1139,12 +1200,12 @@ TURN_THINK_BUDGET_STEER = (
|
|
|
1139
1200
|
def turn_think_budget_check(turn_think_tokens: int, budget: int, half_fired: bool,
|
|
1140
1201
|
exhausted: bool):
|
|
1141
1202
|
"""Threshold latch for the cumulative per-turn reasoning budget: 'half' fires ONCE as
|
|
1142
|
-
a soft steer at budget/2; 'exhausted' fires ONCE as the transition into
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
(
|
|
1147
|
-
|
|
1203
|
+
a soft steer at budget/2; 'exhausted' fires ONCE as the transition into the throttled
|
|
1204
|
+
state (see turn_think_throttle — the TB2.1 v1.0.0 run showed a blanket rest-of-turn
|
|
1205
|
+
no-think mute degrades Ornith's tool-call syntax and capability over long tails,
|
|
1206
|
+
plan 107). Once exhausted, always returns (None, half_fired, True) — nothing further
|
|
1207
|
+
to escalate. Returns (decision, half_fired, exhausted). Pure/testable; the caller
|
|
1208
|
+
owns the state."""
|
|
1148
1209
|
if exhausted:
|
|
1149
1210
|
return None, half_fired, True
|
|
1150
1211
|
if turn_think_tokens >= budget:
|
|
@@ -1154,6 +1215,24 @@ def turn_think_budget_check(turn_think_tokens: int, budget: int, half_fired: boo
|
|
|
1154
1215
|
return None, half_fired, False
|
|
1155
1216
|
|
|
1156
1217
|
|
|
1218
|
+
def turn_think_throttle(turn_think_tokens: int, budget: int, nothink_paid: int,
|
|
1219
|
+
rearm: int = TURN_THINK_REARM_TOK) -> bool:
|
|
1220
|
+
"""Should THIS step render with thinking off? A duty-cycle throttle for the
|
|
1221
|
+
post-exhaustion state: crossing the budget owes one forced no-think action step,
|
|
1222
|
+
and every further `rearm` think tokens spent owes one more. The caller counts the
|
|
1223
|
+
no-think steps it has already paid (`nothink_paid`). Self-correcting by design —
|
|
1224
|
+
a model that keeps burning reasoning is throttled toward always-off (the old
|
|
1225
|
+
persistent-mute behavior), while one that stops over-thinking gets its thinking
|
|
1226
|
+
back once the owed steps are paid. The v1.0.0 full run's evidence for restoring
|
|
1227
|
+
rather than muting: break-filter-js spent 26 straight no-think steps after
|
|
1228
|
+
exhaustion and regressed a run1 pass with garbled landing tool calls (plan 107 F1).
|
|
1229
|
+
Pure/testable; never touches an in-flight generation (086 contract)."""
|
|
1230
|
+
if turn_think_tokens < budget:
|
|
1231
|
+
return False
|
|
1232
|
+
owed = 1 + (turn_think_tokens - budget) // max(1, rearm)
|
|
1233
|
+
return nothink_paid < owed
|
|
1234
|
+
|
|
1235
|
+
|
|
1157
1236
|
# Injected as the review turn's task preamble. A fresh Agent on a
|
|
1158
1237
|
# reset KV cache re-derives the deliverables from a clean context — the poisoned context
|
|
1159
1238
|
# that convinced the first attempt it was done is gone — so it can catch the confident-
|
|
@@ -256,12 +256,14 @@ LEVERS: dict[str, Lever] = {
|
|
|
256
256
|
# off entirely in interactive/unmetered runs, like wrapup_window. -------------
|
|
257
257
|
"turn_think_budget": Lever(
|
|
258
258
|
"A cumulative per-turn reasoning-token budget (wall- and decode-speed-aware, "
|
|
259
|
-
"clamped 8k-24k): a one-shot soft steer at half spent, then a
|
|
260
|
-
"
|
|
261
|
-
"
|
|
262
|
-
"
|
|
263
|
-
"
|
|
264
|
-
"
|
|
259
|
+
"clamped 8k-24k): a one-shot soft steer at half spent, then a no-think "
|
|
260
|
+
"THROTTLE once spent — one forced no-think action step per 3k further think "
|
|
261
|
+
"tokens (guardrails.turn_think_throttle), so thinking restores when the model "
|
|
262
|
+
"stops over-spending; a blanket rest-of-turn mute regressed run1 passes with "
|
|
263
|
+
"garbled no-think tails (plan 107). Acts only at step boundaries on fresh "
|
|
264
|
+
"generations, never an in-flight one (086's lesson). Off in plan mode, for "
|
|
265
|
+
"read-only-intent turns, and below a 300s wall budget (short relaunch tails "
|
|
266
|
+
"belong to hard_wrapup). Only active with a wall budget configured.",
|
|
265
267
|
"iter11"),
|
|
266
268
|
|
|
267
269
|
# --- (103 hard wrap-up): the 085 wrapup_window nudge only lands at a step
|
|
@@ -92,6 +92,16 @@ _ACTION_WORDS = (
|
|
|
92
92
|
"handle", "patch", "correct", "write", "test", "build", "wire", "convert",
|
|
93
93
|
"generate", "rewrite", "extend", "append", "document", "save",
|
|
94
94
|
)
|
|
95
|
+
# Run-task verbs: the ask is to put the SYSTEM in a state (start a service, boot an
|
|
96
|
+
# image, install a package), not to land a file edit — completable with zero edits, so
|
|
97
|
+
# these must NOT feed the `action` class (whose no-empty-diff done gate demands a landed
|
|
98
|
+
# edit; arming it here would hard-block every legitimate zero-edit completion). They get
|
|
99
|
+
# their own `run` class, which arms only the anti-bail nudges (TB2.1 qemu-startup,
|
|
100
|
+
# plan 107 follow-up: classified neither action nor read-only, so a prose give-up with
|
|
101
|
+
# 733s left sailed through the weakest path).
|
|
102
|
+
_RUN_WORDS_RE = re.compile(
|
|
103
|
+
r"\b(start|launch|boot|run|serve|deploy|install|configure|mount|restart|enable"
|
|
104
|
+
r"|spin up|bring up|set up)\b")
|
|
95
105
|
# Explanatory openers: an ask that legitimately ends in prose. Deliberately NARROW —
|
|
96
106
|
# a bare trailing "?" doesn't count (too ambiguous), and "can/could/should you…" are
|
|
97
107
|
# excluded because they're usually polite ACTION requests ("can you fix this?").
|
|
@@ -161,7 +171,11 @@ def classify_intent(user_text: str) -> dict:
|
|
|
161
171
|
or any(p in ut for p in _READ_ONLY_GLOBAL)
|
|
162
172
|
or any(_global_negation(p) for p in _READ_ONLY_NEGATIONS if p in ut))
|
|
163
173
|
action = any(w in ut for w in _ACTION_WORDS)
|
|
164
|
-
|
|
174
|
+
# `run`: a system-state imperative (start/boot/serve/…). Kept SEPARATE from
|
|
175
|
+
# `action` — run tasks are completable with zero file edits, so they arm the
|
|
176
|
+
# anti-bail nudges but never the edit-demanding no-empty-diff gates.
|
|
177
|
+
run = bool(_RUN_WORDS_RE.search(ut)) and not explanatory
|
|
178
|
+
return {"action": action, "read_only": read_only, "run": run}
|
|
165
179
|
|
|
166
180
|
|
|
167
181
|
# Short base prompt for a spawned sub-agent. It runs in a fresh, isolated
|
|
@@ -104,6 +104,27 @@ def _ts_error_loc(text: str, line: int) -> str:
|
|
|
104
104
|
# error delta, not on every still-broken landing, so a streak there is unobservable.
|
|
105
105
|
_BROKEN_STREAK: dict[str, int] = {}
|
|
106
106
|
|
|
107
|
+
# Prose/data formats whose tree-sitter grammars exist but whose "syntax errors" are
|
|
108
|
+
# noise for this gate: the language pack maps `.txt` to VIMDOC, so plain-text
|
|
109
|
+
# deliverable writes (answer.txt, secret.txt, requirements.txt — the TB2.1 run1 README
|
|
110
|
+
# finding, plan 107 follow-up) got grammar-checked and warned on exactly the
|
|
111
|
+
# deliverable-landing write. A missing entry here costs a spurious warning, never a
|
|
112
|
+
# missed real one — code languages are not listed.
|
|
113
|
+
_NON_CODE_LANGS = frozenset({
|
|
114
|
+
"vimdoc", "markdown", "markdown_inline", "csv", "tsv", "psv", "text", "rst",
|
|
115
|
+
"org", "diff", "gitcommit", "git_rebase", "gitattributes", "gitignore",
|
|
116
|
+
"properties", "requirements",
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _code_lang(path: str) -> str | None:
|
|
121
|
+
"""`lang_for`, filtered to languages this gate should police: None for both
|
|
122
|
+
unknown extensions and the prose/data formats above — every gate in this module
|
|
123
|
+
(warn, edit-revert, write-reject, severed-span) must skip those uniformly, or a
|
|
124
|
+
plain-text deliverable write gets warned on (or worse, REVERTED)."""
|
|
125
|
+
detected = repomap.service().lang_for(path)
|
|
126
|
+
return None if detected in _NON_CODE_LANGS else detected
|
|
127
|
+
|
|
107
128
|
|
|
108
129
|
def check_syntax(path: str, before: str | None) -> str | None:
|
|
109
130
|
"""A warning string when the current on-disk content of `path` has a *newly
|
|
@@ -125,7 +146,7 @@ def check_syntax(path: str, before: str | None) -> str | None:
|
|
|
125
146
|
if after == before: # the tool didn't actually change the file — nothing to flag
|
|
126
147
|
return None
|
|
127
148
|
|
|
128
|
-
lang =
|
|
149
|
+
lang = _code_lang(path)
|
|
129
150
|
|
|
130
151
|
if lang == "python":
|
|
131
152
|
ap = os.path.abspath(path)
|
|
@@ -251,7 +272,7 @@ def edit_reject(path: str, before: str, after: str,
|
|
|
251
272
|
return None
|
|
252
273
|
if len(after) > _MAX_BYTES:
|
|
253
274
|
return None
|
|
254
|
-
lang =
|
|
275
|
+
lang = _code_lang(path)
|
|
255
276
|
if not lang:
|
|
256
277
|
return None
|
|
257
278
|
if lang != "python":
|
|
@@ -359,7 +380,7 @@ def write_reject(path: str, before: str | None, content: str) -> str | None:
|
|
|
359
380
|
return None
|
|
360
381
|
if len(content) > _MAX_BYTES or (before is not None and len(before) > _MAX_BYTES):
|
|
361
382
|
return None
|
|
362
|
-
lang =
|
|
383
|
+
lang = _code_lang(path)
|
|
363
384
|
if not lang:
|
|
364
385
|
return None
|
|
365
386
|
bash_hint = ("If this file is SUPPOSED to contain invalid syntax (a test fixture), "
|
|
@@ -568,7 +589,7 @@ def drift_warn(path: str, before: str | None, after: str) -> str | None:
|
|
|
568
589
|
return None
|
|
569
590
|
if before is None or len(before) > _MAX_BYTES or len(after) > _MAX_BYTES:
|
|
570
591
|
return None
|
|
571
|
-
lang =
|
|
592
|
+
lang = _code_lang(path)
|
|
572
593
|
if not lang:
|
|
573
594
|
return None
|
|
574
595
|
dropped: list[str] = []
|
|
@@ -758,34 +758,66 @@ class _BigThinkEngine(ScriptedEngine):
|
|
|
758
758
|
return text, stats
|
|
759
759
|
|
|
760
760
|
|
|
761
|
-
def _run_turn_think_budget(monkeypatch, *, ablated: bool
|
|
761
|
+
def _run_turn_think_budget(monkeypatch, *, ablated: bool, script=None,
|
|
762
|
+
turn_budget_s=1e9):
|
|
762
763
|
if ablated:
|
|
763
764
|
monkeypatch.setenv("CHAD_DISABLE", "turn_think_budget")
|
|
764
765
|
else:
|
|
765
766
|
monkeypatch.delenv("CHAD_DISABLE", raising=False)
|
|
766
767
|
stall = "<think>" + "reasoning " * 50 + "</think>"
|
|
767
|
-
eng = _BigThinkEngine([stall] * 6)
|
|
768
|
+
eng = _BigThinkEngine(script if script is not None else [stall] * 6)
|
|
768
769
|
tok = _ThinkFlagTok()
|
|
769
770
|
eng.tok = tok
|
|
770
771
|
# A configured wall budget is required to arm the mechanism at all (like
|
|
771
772
|
# wrapup_window); huge so the UNRELATED governor/wrapup checks never fire and
|
|
772
773
|
# muddy the transcript this test inspects.
|
|
773
774
|
agent = Agent(eng, mode="auto", thinking=True, max_gen_tokens=20000,
|
|
774
|
-
turn_budget_s=
|
|
775
|
+
turn_budget_s=turn_budget_s)
|
|
775
776
|
agent.run_turn("change the config value") # an action task, not read-only
|
|
776
777
|
return tok.flags
|
|
777
778
|
|
|
778
779
|
|
|
779
|
-
def
|
|
780
|
+
def test_turn_think_budget_throttles_no_think_while_overspending(monkeypatch):
|
|
780
781
|
flags = _run_turn_think_budget(monkeypatch, ablated=False)
|
|
781
782
|
# Each stall's ~14.7k-token think delta clears HALF (12k) on step 0 and EXHAUSTS
|
|
782
783
|
# (24k) by step 1 — so step 0/1 still render thinking (the mechanism only acts on
|
|
783
|
-
# steps AFTER the threshold is crossed)
|
|
784
|
+
# steps AFTER the threshold is crossed). The scripted engine keeps burning ~14.7k
|
|
785
|
+
# think tokens on EVERY later step too, so the throttle's debt (one no-think step
|
|
786
|
+
# per 3k further think tokens) grows faster than it is paid — every remaining step
|
|
787
|
+
# renders no-think, converging on the old persistent behavior for a model that
|
|
788
|
+
# never stops over-spending.
|
|
784
789
|
assert flags[:2] == [True, True], flags
|
|
785
790
|
assert flags[2:] and all(f is False for f in flags[2:]), flags
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
791
|
+
|
|
792
|
+
|
|
793
|
+
def test_turn_think_budget_restores_thinking_once_debt_paid(monkeypatch):
|
|
794
|
+
# Two big-think stalls exhaust the budget (~29.4k vs 24k -> debt = 1 + 5.4k//3k
|
|
795
|
+
# = 2 no-think steps owed); the model then STOPS thinking (plain tool-call turns
|
|
796
|
+
# accrue zero think delta), so after the two owed steps are paid, thinking must
|
|
797
|
+
# RESTORE — the plan-107 fix over the v1.0.0 persistent mute, which stayed off for
|
|
798
|
+
# the rest of the turn (break-filter-js: 26 straight no-think steps, garbled
|
|
799
|
+
# landing calls, regressed a run1 pass).
|
|
800
|
+
stall = "<think>" + "reasoning " * 50 + "</think>"
|
|
801
|
+
tc = _tool_call("bash", command="echo ok")
|
|
802
|
+
flags = _run_turn_think_budget(
|
|
803
|
+
monkeypatch, ablated=False,
|
|
804
|
+
script=[stall, stall, tc, tc, tc, "All set — the config value is updated."])
|
|
805
|
+
assert flags[:2] == [True, True], flags
|
|
806
|
+
assert flags[2:4] == [False, False], flags # paying the two owed steps
|
|
807
|
+
assert flags[4:] and all(f is True for f in flags[4:]), flags # restored
|
|
808
|
+
|
|
809
|
+
|
|
810
|
+
def test_turn_think_budget_inert_below_min_wall(monkeypatch):
|
|
811
|
+
# A short relaunch tail (wall budget under TURN_THINK_MIN_WALL_S=300) must leave
|
|
812
|
+
# the mechanism entirely inert — on the v1.0.0 run a 200s-class relaunch clamped
|
|
813
|
+
# to the LO budget and half-fired on its FIRST step, churning against the hard
|
|
814
|
+
# wrap-up landing (regex-log). The governor/wrapup paths may also act on such a
|
|
815
|
+
# short wall, so keep the script tiny.
|
|
816
|
+
stall = "<think>" + "reasoning " * 50 + "</think>"
|
|
817
|
+
flags = _run_turn_think_budget(monkeypatch, ablated=False,
|
|
818
|
+
script=[stall] * 3, turn_budget_s=200)
|
|
819
|
+
assert flags, "the turn should have taken at least one step"
|
|
820
|
+
assert False not in flags, flags
|
|
789
821
|
|
|
790
822
|
|
|
791
823
|
def test_turn_think_budget_is_gone_when_ablated(monkeypatch):
|
|
@@ -49,11 +49,13 @@ from chad.guardrails import (
|
|
|
49
49
|
progress_note,
|
|
50
50
|
relaunch_budget,
|
|
51
51
|
repeat_stop_abort,
|
|
52
|
+
replenish_continue,
|
|
52
53
|
review_pass_should_fire,
|
|
53
54
|
think_budget,
|
|
54
55
|
turn_governor,
|
|
55
56
|
turn_think_budget,
|
|
56
57
|
turn_think_budget_check,
|
|
58
|
+
turn_think_throttle,
|
|
57
59
|
update_thrash,
|
|
58
60
|
update_work_flags,
|
|
59
61
|
wrapup_landing_steer,
|
|
@@ -652,6 +654,53 @@ def test_progress_note():
|
|
|
652
654
|
+ '<tool_call>{"name": "read", "arguments": {"path": "z.py"}}</tool_call>'}]
|
|
653
655
|
ln = progress_note(loopy)
|
|
654
656
|
check("degenerate reasoning not carried as hypothesis", "hypothesis" not in ln.lower(), ln)
|
|
657
|
+
# Rejected-completion-claim path (plan 107 follow-up — the build-pov-ray poisoning
|
|
658
|
+
# loop: the banked note carried "the build is already complete and verified" as its
|
|
659
|
+
# leading hypothesis, and all 6 relaunches re-confirmed it and re-done'd).
|
|
660
|
+
poisoned = [
|
|
661
|
+
{"role": "user", "content": "build povray"},
|
|
662
|
+
{"role": "assistant",
|
|
663
|
+
"content": ("The build is already complete and verified. Let me confirm.\n"
|
|
664
|
+
'<tool_call>{"name": "bash", "arguments": {"command": "ls build"}}'
|
|
665
|
+
"</tool_call>")},
|
|
666
|
+
{"role": "tool", "name": "bash", "content": "povray"},
|
|
667
|
+
]
|
|
668
|
+
rejected = progress_note(poisoned, rejected_claim="POV-Ray 2.2 is already built")
|
|
669
|
+
check("rejected claim -> warning present", "REJECTED" in rejected, rejected)
|
|
670
|
+
check("rejected claim text is quoted (clipped)",
|
|
671
|
+
"already built" in rejected, rejected)
|
|
672
|
+
check("completion-claim hypothesis is dropped",
|
|
673
|
+
"already complete and verified" not in rejected, rejected)
|
|
674
|
+
# A genuine (non-completion) diagnosis survives even on the rejected path.
|
|
675
|
+
diagnosed = list(poisoned)
|
|
676
|
+
diagnosed[1] = {"role": "assistant",
|
|
677
|
+
"content": ("The linker fails because unix.c is missing from build/.\n"
|
|
678
|
+
'<tool_call>{"name": "bash", "arguments": '
|
|
679
|
+
'{"command": "ls build"}}</tool_call>')}
|
|
680
|
+
kept = progress_note(diagnosed, rejected_claim="done")
|
|
681
|
+
check("real diagnosis survives the rejected path",
|
|
682
|
+
"unix.c is missing" in kept, kept)
|
|
683
|
+
# FP class the first regex dropped (eng-review outside-voice F1): a diagnosis that
|
|
684
|
+
# merely CONTAINS a success-ish substring is not a completion claim and must survive.
|
|
685
|
+
# "rebuild is failing" once matched bare `build\s+is\s`; "successfully reproduced"
|
|
686
|
+
# once matched the standalone verified/successful branch.
|
|
687
|
+
for diag_text, needle in (
|
|
688
|
+
("The rebuild is failing because unix.c is missing from the target list.",
|
|
689
|
+
"rebuild is failing"),
|
|
690
|
+
("I successfully reproduced the crash: it is an off-by-one in the parse loop.",
|
|
691
|
+
"off-by-one in the parse loop"),
|
|
692
|
+
("Verified the repro; the bug is a null deref in frame_alloc.",
|
|
693
|
+
"null deref in frame_alloc"),
|
|
694
|
+
):
|
|
695
|
+
d = list(poisoned)
|
|
696
|
+
d[1] = {"role": "assistant",
|
|
697
|
+
"content": (diag_text + '\n<tool_call>{"name": "bash", "arguments": '
|
|
698
|
+
'{"command": "ls build"}}</tool_call>')}
|
|
699
|
+
note = progress_note(d, rejected_claim="task is done")
|
|
700
|
+
check(f"diagnosis survives rejected path: {needle!r}", needle in note, note)
|
|
701
|
+
# Without rejected_claim the note is unchanged from the old contract: no warning.
|
|
702
|
+
check("no rejected claim -> no warning line",
|
|
703
|
+
"REJECTED" not in progress_note(poisoned), progress_note(poisoned))
|
|
655
704
|
|
|
656
705
|
|
|
657
706
|
def test_bash_result_verifies_ignores_trivial_checks():
|
|
@@ -902,6 +951,41 @@ def test_turn_think_budget_check():
|
|
|
902
951
|
check("exhausted is sticky", d is None and half and exh)
|
|
903
952
|
|
|
904
953
|
|
|
954
|
+
def test_turn_think_throttle():
|
|
955
|
+
# Below budget: never throttled, regardless of paid count.
|
|
956
|
+
check("below budget -> no throttle", turn_think_throttle(7999, 8000, 0) is False)
|
|
957
|
+
# Crossing the budget owes exactly ONE no-think step until paid.
|
|
958
|
+
check("at budget, unpaid -> throttle", turn_think_throttle(8000, 8000, 0) is True)
|
|
959
|
+
check("at budget, paid -> restored", turn_think_throttle(8000, 8000, 1) is False)
|
|
960
|
+
# Every further `rearm` think tokens owes one more: 8000 + 2*3000 = 3 owed total.
|
|
961
|
+
check("2 rearm chunks past budget, 2 paid -> still owes one",
|
|
962
|
+
turn_think_throttle(14000, 8000, 2) is True)
|
|
963
|
+
check("2 rearm chunks past budget, 3 paid -> restored",
|
|
964
|
+
turn_think_throttle(14000, 8000, 3) is False)
|
|
965
|
+
# A model that keeps burning reasoning is throttled toward always-off; one that
|
|
966
|
+
# stops spending accrues no new debt and keeps its thinking once paid up.
|
|
967
|
+
check("huge overshoot, few paid -> throttle",
|
|
968
|
+
turn_think_throttle(50000, 8000, 5) is True)
|
|
969
|
+
# rearm=0 must not divide by zero (guarded to 1).
|
|
970
|
+
check("zero rearm guarded", turn_think_throttle(9000, 8000, 10**6, rearm=0) is False)
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def test_replenish_continue():
|
|
974
|
+
# More than half the wall unspent and under the cap -> grant.
|
|
975
|
+
check("early giveup, wall mostly unspent -> grant",
|
|
976
|
+
replenish_continue(12000, 637, 2) is True)
|
|
977
|
+
# Past the half-wall point -> no extras (the endgame belongs to wrap-up/landing).
|
|
978
|
+
check("more than half spent -> no grant", replenish_continue(900, 500, 2) is False)
|
|
979
|
+
# Exactly at the boundary is NOT more-than -> no grant.
|
|
980
|
+
check("exact half boundary -> no grant", replenish_continue(1000, 500, 2) is False)
|
|
981
|
+
# Absolute cap: never more than AUTO_CONTINUE_TOTAL_CAP total relaunches.
|
|
982
|
+
check("at the total cap -> no grant", replenish_continue(12000, 100, 6) is False)
|
|
983
|
+
check("just under the cap -> grant", replenish_continue(12000, 100, 5) is True)
|
|
984
|
+
# No wall budget (interactive) -> never; the explicit base allowance governs.
|
|
985
|
+
check("no wall budget -> never", replenish_continue(None, 0, 0) is False)
|
|
986
|
+
check("zero wall budget -> never", replenish_continue(0, 0, 0) is False)
|
|
987
|
+
|
|
988
|
+
|
|
905
989
|
def test_review_pass_should_fire():
|
|
906
990
|
# Clean end + a wall budget + >30% unspent -> fire. 900s budget, 500s elapsed:
|
|
907
991
|
# 500 < 0.7*900 = 630 -> more than 30% left.
|
|
@@ -102,6 +102,32 @@ def test_intent():
|
|
|
102
102
|
not arms_nudge("explain the slugify function"))
|
|
103
103
|
|
|
104
104
|
|
|
105
|
+
def test_run_intent():
|
|
106
|
+
# Run-task class (plan 107 follow-up): system-state imperatives — completable with
|
|
107
|
+
# zero file edits — must classify `run` so the anti-bail nudges arm, WITHOUT
|
|
108
|
+
# feeding `action` (whose no-empty-diff done gate demands a landed edit).
|
|
109
|
+
qemu = ("Start the /app/alpine.iso image in qemu in such a way that I can connect "
|
|
110
|
+
"to it via `telnet 127.0.0.1 6665`. Start the image in the background and "
|
|
111
|
+
"leave it running. Block until it's ready.")
|
|
112
|
+
got = classify_intent(qemu)
|
|
113
|
+
check("qemu-startup is a run task", got["run"], got)
|
|
114
|
+
check("qemu-startup is NOT an action task (no edit demanded)",
|
|
115
|
+
not got["action"], got)
|
|
116
|
+
check("qemu-startup is not read-only", not got["read_only"], got)
|
|
117
|
+
check("service imperative is a run task",
|
|
118
|
+
classify_intent("install nginx and serve /var/www on port 80")["run"])
|
|
119
|
+
# Word boundary: 'restarting'/'rerun' style inflections must not leak via substring.
|
|
120
|
+
check("'brunch' does not classify as run",
|
|
121
|
+
not classify_intent("describe the brunch menu parser")["run"])
|
|
122
|
+
# Explanatory asks never classify run — 'explain how to start the server' ends in prose.
|
|
123
|
+
check("explain-how-to-start is not a run task",
|
|
124
|
+
not classify_intent("explain how to start the server")["run"])
|
|
125
|
+
# An edit ask that also says 'run the tests' keeps action AND gains run — harmless,
|
|
126
|
+
# the merged nudge predicate is already armed by action.
|
|
127
|
+
both = classify_intent("fix the bug in app.py and run the tests")
|
|
128
|
+
check("edit+run ask keeps action", both["action"] and both["run"], both)
|
|
129
|
+
|
|
130
|
+
|
|
105
131
|
def test_open_tool_call():
|
|
106
132
|
# truncated mid-call -> True (drives the "write it in parts" nudge)
|
|
107
133
|
check("open <tool_call> detected",
|
|
@@ -266,6 +292,7 @@ def test_step_tool_cap():
|
|
|
266
292
|
|
|
267
293
|
if __name__ == "__main__":
|
|
268
294
|
test_intent()
|
|
295
|
+
test_run_intent()
|
|
269
296
|
test_open_tool_call()
|
|
270
297
|
test_mentions()
|
|
271
298
|
test_detect_test_command()
|
|
@@ -156,8 +156,33 @@ def test_indent_reject_names_enclosing_symbol():
|
|
|
156
156
|
check("module-level: no enclosing symbol steer", "You're editing inside" not in msg2, msg2)
|
|
157
157
|
|
|
158
158
|
|
|
159
|
+
def test_plain_text_never_policed():
|
|
160
|
+
# The language pack maps .txt to VIMDOC, so plain-text deliverable writes
|
|
161
|
+
# (answer.txt / secret.txt / requirements.txt — the TB2.1 run1 README finding,
|
|
162
|
+
# plan 107 follow-up) were grammar-checked and warned on exactly the
|
|
163
|
+
# deliverable-landing write. Prose/data formats are now excluded from every gate.
|
|
164
|
+
for name, content in (
|
|
165
|
+
("answer.txt", "The flag is: ABC-123\nsecond line < > { weird ] chars\n"),
|
|
166
|
+
("requirements.txt", "numpy>=1.24\nscipy==1.11.*\n"),
|
|
167
|
+
("notes.md", "# heading\n<unclosed <tag [bracket\n"),
|
|
168
|
+
("data.csv", 'a,b\n1,"unclosed quote\n'),
|
|
169
|
+
):
|
|
170
|
+
p = _tmp(name, "")
|
|
171
|
+
res = tool_write(p, content)
|
|
172
|
+
check(f"{name} write lands", res.startswith("[wrote"), res)
|
|
173
|
+
check(f"{name} write has no syntax warning",
|
|
174
|
+
"warning" not in res and "rejected" not in res, res)
|
|
175
|
+
res = tool_edit(p, content.splitlines()[0], "replaced first line")
|
|
176
|
+
check(f"{name} edit lands without warning",
|
|
177
|
+
"warning" not in res and "rejected" not in res, res)
|
|
178
|
+
# Sanity: real code languages are still policed (guard against an over-broad list).
|
|
179
|
+
check("python still policed",
|
|
180
|
+
syntaxgate.write_reject("x.py", "", "def f(:\n") is not None)
|
|
181
|
+
|
|
182
|
+
|
|
159
183
|
if __name__ == "__main__":
|
|
160
184
|
test_python()
|
|
185
|
+
test_plain_text_never_policed()
|
|
161
186
|
test_tree_sitter_delta()
|
|
162
187
|
test_opt_out()
|
|
163
188
|
test_symbol_edit()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|