Runs that go in circles end (M7.8)
CI / checks (push) Skipped
CI / checks (pull_request) Failing after 1m50s
CI / e2e (pull_request) Skipped

Driver.run checks each turn with nisse_core.loop.circles and ends the run
as "circling" when the same calls get much the same back, one harness
correction is given again and again, or the model apologises, for
circle_repeats turns in a row (default 3). Near-identical calls (git add
a.py, b.py) and scattered apologies stay flags on the status page. A
fact in the transcript says why the run ended.

nisse-dev calls in a helper as for a stuck run, then asks the operator;
nisse-review counts a failed run and posts none of its drafts. Tuning:
tuning.driver / tuning.review circle_repeats (0 off) and circle_similar.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
This commit is contained in:
dongarandClaude Opus 5.5 committed 2026-10-26 16:30:07 +00:00
1 parent 516a328618
commit 74bc68db1c
20 files changed
+506 -20

No files matched your search

+44 -1
View File
@@ -1,6 +1,6 @@
"""Runs going in circles (L2, X6), judged from the turns alone."""
from nisse_core.loop.circles import Turn, find_circles
from nisse_core.loop.circles import Turn, apologies_in_a_row, find_circles
NUDGE = "Your last reply was prose. Every reply must be a tool call."
@@ -71,3 +71,46 @@ def test_the_limits_are_tuning() -> None:
turns = [comment(1), comment(2)]
assert [c.kind for c in find_circles(turns, repeats=2)] == ["repeated reply"]
assert find_circles([comment(1), comment(22), comment(333)], similar=1.0) == []
def test_alike_replies_that_got_different_results_are_no_circle() -> None:
read = [("read_file", '{"path": "src/a.py"}')]
turns = [Turn(calls=tuple(read), results=(f"file {n}",)) for n in range(4)]
assert find_circles(turns) == []
same = [Turn(calls=tuple(read), results=("posted",)) for _ in range(4)]
assert [c.kind for c in find_circles(same)] == ["repeated reply"]
# The page gives no results: alike replies alone are a circle there.
bare = [Turn(calls=tuple(read)) for _ in range(3)]
assert [c.kind for c in find_circles(bare)] == ["repeated reply"]
def test_the_driver_counts_only_exactly_repeated_calls() -> None:
adds = [
Turn(calls=(("run_command", f'{{"command": "git add {f}.py"}}'),))
for f in "abc"
]
assert [c.kind for c in find_circles(adds)] == ["repeated reply"] # flagged
assert find_circles(adds, exact_calls=True) == [] # never ended for it
same = [Turn(calls=(("read_file", '{"path": "a"}'),))] * 3
assert len(find_circles(same, exact_calls=True)) == 1
def test_apologies_end_a_run_only_in_a_row() -> None:
sorry, work = Turn(text="Sorry, my mistake."), Turn(text="Reading the file.")
assert apologies_in_a_row([work, sorry, sorry, sorry]) is not None
assert apologies_in_a_row([sorry, work, sorry, sorry]) is None
assert apologies_in_a_row([sorry, sorry]) is None
# In what it posted, too: an apology comment is an apology.
posted = Turn(calls=(("post_comment", '{"body": "Please disregard (1)."}'),))
assert apologies_in_a_row([posted] * 3) is not None
def test_the_same_call_answered_with_a_count_is_still_a_circle() -> None:
note = (("comment", '{"line": 2, "text": "Same point."}'),)
turns = [Turn(calls=note, results=(f"noted ({n} in the draft)",)) for n in range(3)]
assert len(find_circles(turns, exact_calls=True)) == 1
files = [
Turn(calls=(("read_file", f'{{"path": "{f}"}}'),), results=(f"{f}: x",))
for f in "abc"
]
assert find_circles(files, exact_calls=True) == []