Before each driver call, over compact_at (0.6) of the prompt budget, the same model is asked for notes over the very request it was about to get, plus one message, so the server's cache covers all but the notes. The notes, with code's list of the calls they replace, become a compaction event that renders in place of its range, keeping word for word what started the work and the last few turns. Rare, large steps keep the prompt's start unchanged between them. A later compaction folds in the earlier; one from an earlier run's view is hidden. Failed notes are logged and fitting takes over. Fitting now applies compactions before it measures, records what it cuts (Trim) and logs each cut as context.trimmed. Wired into both agents' drivers and the benchmark; nisse-dev's helper loop is a follow-up. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
173 lines
6.4 KiB
Python
173 lines
6.4 KiB
Python
"""A clean stop (L2): SIGTERM ends a review run at its next turn, or breaks
|
|
off its model call, and the item goes back to ready with no failure counted;
|
|
a repository Gitea won't list is logged when that changes, not every round."""
|
|
|
|
import io
|
|
import json
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from review_fakes import FakeGitea, Scripted, call, pr
|
|
from test_review import REPO, REVIEW, item, world
|
|
|
|
from nisse_core.models.interruptible import Interruptible, Stopped
|
|
from nisse_core.oplog import OpLog
|
|
from nisse_review.build import build
|
|
from nisse_review.config import ReviewConfig
|
|
from nisse_review.review import reply_marker
|
|
from nisse_review.state import READY, REVIEWED, pr_of
|
|
|
|
|
|
class Stopping(Scripted):
|
|
"""Scripted, but at call ``at`` (0-based) it first runs ``then``: a stop
|
|
arriving while the model works on that turn."""
|
|
|
|
def __init__(self, *turns: list[Any], at: int) -> None:
|
|
super().__init__(*turns)
|
|
self.at = at
|
|
self.then: Callable[[], None] = lambda: None
|
|
self.calls = 0
|
|
|
|
def complete(self, events, tools, model, system=None): # type: ignore[no-untyped-def]
|
|
now, self.calls = self.calls, self.calls + 1
|
|
if now == self.at:
|
|
self.then()
|
|
return super().complete(events, tools, model, system)
|
|
|
|
|
|
def events(out: io.StringIO, name: str) -> list[dict[str, Any]]:
|
|
found = (json.loads(line) for line in out.getvalue().splitlines())
|
|
return [e for e in found if e["event"] == name]
|
|
|
|
|
|
def logged(w: dict[str, Any]) -> io.StringIO:
|
|
out = io.StringIO()
|
|
w["scheduler"]._oplog = OpLog("t", out)
|
|
return out
|
|
|
|
|
|
def test_a_stop_ends_the_run_at_its_next_turn(tmp_path: Path) -> None:
|
|
model = Stopping(*REVIEW, at=1)
|
|
w = world(tmp_path, model)
|
|
model.then = w["scheduler"].stop
|
|
out = logged(w)
|
|
assert w["scheduler"].round() is True
|
|
assert model.calls == 2 # the turn in flight finished; no third
|
|
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 0)
|
|
assert pr_of(item(w)).run == "" and w["gitea"].posted == []
|
|
assert len(events(out, "review.interrupted")) == 1
|
|
assert events(out, "review.failed") == []
|
|
w["scheduler"].run_forever() # stopped: returns at once
|
|
|
|
|
|
def test_a_stop_breaks_off_the_model_call_in_flight(tmp_path: Path) -> None:
|
|
model = Stopping(*REVIEW, at=1)
|
|
wrapped = Interruptible(model)
|
|
w = world(tmp_path, wrapped) # type: ignore[arg-type]
|
|
|
|
def sigterm() -> None: # what Built.stop does, in the signal handler
|
|
w["scheduler"].stop()
|
|
wrapped.interrupt()
|
|
|
|
model.then = sigterm
|
|
out = logged(w)
|
|
assert w["scheduler"].round() is True
|
|
assert (item(w).position, pr_of(item(w)).attempts) == (READY, 0)
|
|
assert w["gitea"].posted == []
|
|
[said] = events(out, "review.interrupted")
|
|
assert "broke off the model call" in said["cause"]
|
|
|
|
|
|
def test_outside_a_model_call_interrupt_does_nothing() -> None:
|
|
wrapped = Interruptible(Scripted([]))
|
|
wrapped.interrupt() # nothing in flight: no Stopped
|
|
assert wrapped.complete([], None, "m") == []
|
|
wrapped.interrupt() # the call is over: still nothing
|
|
assert wrapped.offered == [set()] # the rest reaches the client
|
|
|
|
|
|
def test_stopped_is_no_failure_handlers_would_take() -> None:
|
|
assert not issubclass(Stopped, Exception)
|
|
|
|
|
|
def test_replies_posted_before_a_stop_are_not_posted_again(tmp_path: Path) -> None:
|
|
model = Stopping(
|
|
*REVIEW,
|
|
[call("reply", point="P1", text="Tabs never occur here.")],
|
|
[call("reply", point="P2", text="It counts words, yes.")], # stopped here
|
|
[call("reply", point="P1", text="It counts words, yes.")],
|
|
[call("done")],
|
|
at=5,
|
|
)
|
|
gitea = FakeGitea(repos={REPO: [pr()]})
|
|
w = world(tmp_path, model, gitea, operators=frozenset({"don"}))
|
|
model.then = w["scheduler"].stop
|
|
assert w["scheduler"].round() is True
|
|
first = w["gitea"].say("nisse-dev", "Why flag split()?")
|
|
second = w["gitea"].say("nisse-dev", "Does it count words?")
|
|
assert w["scheduler"].round() is True # replied to P1, then stopped
|
|
assert item(w).position == READY
|
|
[(_, _, posted)] = w["gitea"].comments
|
|
assert reply_marker(f"comment-{first}") in posted
|
|
w["scheduler"]._stop.clear()
|
|
w["scheduler"]._cancel.clear()
|
|
assert w["scheduler"].round() is True # only the second is left
|
|
bodies = [body for _, _, body in w["gitea"].comments]
|
|
assert sum(reply_marker(f"comment-{first}") in b for b in bodies) == 1
|
|
assert sum(reply_marker(f"comment-{second}") in b for b in bodies) == 1
|
|
assert item(w).position == REVIEWED
|
|
|
|
|
|
# --- a repository Gitea won't list ---------------------------------------------
|
|
|
|
|
|
def test_a_repository_that_keeps_failing_is_logged_once(tmp_path: Path) -> None:
|
|
gitea = FakeGitea(repos={"a/empty": [], REPO: []}, unlisted={"a/empty"})
|
|
w = world(tmp_path, Scripted(), gitea)
|
|
out = logged(w)
|
|
for _ in range(3):
|
|
w["scheduler"].round()
|
|
assert len(events(out, "intake.repo_failed")) == 1
|
|
gitea.unlisted.clear()
|
|
w["scheduler"].round()
|
|
w["scheduler"].round()
|
|
[ok] = events(out, "intake.repo_ok")
|
|
assert ok["repo"] == "a/empty"
|
|
gitea.unlisted.add("a/empty") # failing again is news again
|
|
w["scheduler"].round()
|
|
assert len(events(out, "intake.repo_failed")) == 2
|
|
|
|
|
|
def test_build_wires_a_stop_to_every_model_call(tmp_path: Path) -> None:
|
|
config = ReviewConfig.from_tree(
|
|
{
|
|
"state_db": str(tmp_path / "state.db"),
|
|
"llm": {"base_url": "http://llm.test", "token": "t"},
|
|
"roles": {"reviewer": "m", "expert": "e"},
|
|
"gitea": {
|
|
"url": "https://gitea.test",
|
|
"token": "t",
|
|
"operators": ["don"],
|
|
},
|
|
"repositories": [REPO],
|
|
},
|
|
{},
|
|
)
|
|
built = build(
|
|
config,
|
|
gitea=FakeGitea(repos={REPO: []}), # type: ignore[arg-type]
|
|
client=Scripted(),
|
|
oplog=OpLog("t", io.StringIO()),
|
|
)
|
|
try:
|
|
driver = built.scheduler._reviewer._driver
|
|
assert driver._client is built.model
|
|
# Compaction's notes are a model call a stop breaks off too.
|
|
assert driver._compactor._client is built.model
|
|
assert built.scheduler._reviewer._expert._client is built.model
|
|
built.stop() # no call in flight: nothing raised
|
|
assert driver._cancel.requested and built.scheduler._stop.is_set()
|
|
finally:
|
|
built.close()
|