feat: add adaptive review effort budgets #19

Merged
masi merged 4 commits from feat/review-budget-governor into main 2026-09-01 12:15:17 +00:00
5 changed files with 60 additions and 2 deletions
Showing only changes of commit 4d51e14a1a - Show all commits
+5
View File
@@ -157,6 +157,11 @@ overridden deployment-wide with `PRAGENT_MAX_REVIEW_STEPS`,
`PRAGENT_REVIEW_TIMEOUT`, or per repository in the trusted base-branch
`.pr-review.json`; repository values win.
For repositories with broad diffs, the pilot automatically raises headroom to
40/400K, 60/800K, or 80/1.2M steps/tokens as changed lines cross 200, 800, or
2,000. Explicit repository budgets always take precedence, and the global
hard ceilings remain in force.
Two measured reviews of a ~1100-line PR in this repo: 28 and 31 agent steps,
~2.1M input tokens each, **zero cache reads or writes**. The demo repo's PR, same
tier: 126K tokens.
+5
View File
@@ -306,6 +306,11 @@ limit, preserves any output already emitted, and records the cap reason in the
review body and Langfuse metadata. Environment variables provide deployment-wide
defaults; repository budget values override them.
Without an explicit repository budget, changed diffs receive adaptive headroom:
the default 20-step/120K-token budget grows to 40/400K, 60/800K, or 80/1.2M for
diffs over 200, 800, or 2,000 changed lines. This keeps focused PRs inexpensive
while allowing broad TypeScript/Go reviews to finish. Hard ceilings still apply.
**Cross-lens dedup:** synthesiser drops duplicates by
`sha256[:16](path|line|severity|problem[:80])` (matches the feedback DB's
`posthash`), then promotes multi-lens agreement by one severity step
+35
View File
@@ -15,6 +15,13 @@ DEFAULTS = {
"max_duration_seconds": 480,
}
PROFILES = (
# (changed lines threshold, steps, total tokens, output tokens, seconds)
(2_000, 80, 1_200_000, 80_000, 1_800),
(800, 60, 800_000, 60_000, 1_200),
(200, 40, 400_000, 40_000, 900),
)
@dataclass(frozen=True)
class Budget:
@@ -63,6 +70,34 @@ class Budget:
price_target=price_target,
)
@classmethod
def for_review(cls, config: dict | None, diff: str) -> "Budget":
"""Choose safe headroom from diff size, unless config is explicit."""
if isinstance((config or {}).get("budget"), dict):
return cls.from_config(config)
changed_lines = _changed_line_count(diff)
for threshold, steps, tokens, output, seconds in PROFILES:
if changed_lines >= threshold:
return cls.from_config({
**(config or {}),
"budget": {
"max_steps": steps,
"max_total_tokens": tokens,
"max_output_tokens": output,
"max_duration_seconds": seconds,
},
})
return cls.from_config(config)
def _changed_line_count(diff: str) -> int:
"""Count changed lines without treating hunk headers as additions."""
return sum(
1 for line in diff.splitlines()
if (line.startswith("+") and not line.startswith("+++"))
or (line.startswith("-") and not line.startswith("---"))
)
def _env_int(name: str, default: int) -> int:
try:
+2 -2
View File
@@ -440,7 +440,7 @@ def run_lenses_review(
always has: prose + a final ```json fence with the legacy schema).
"""
os.makedirs(WORK_ROOT, exist_ok=True)
budget = budget or Budget.from_config(config)
budget = budget or Budget.for_review(config, diff)
budget_state = budget_state or BudgetState(budget)
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
keep = bool(os.environ.get("PRAGENT_KEEP_WORK"))
@@ -687,7 +687,7 @@ def run(
additional_context=additional_context,
)
budget = Budget.from_config(config)
budget = Budget.for_review(config, diff)
budget_state = BudgetState(budget)
os.makedirs(WORK_ROOT, exist_ok=True)
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
+13
View File
@@ -18,6 +18,19 @@ def test_budget_reads_config_over_environment(monkeypatch):
assert budget.max_steps == 7
def test_budget_scales_for_broad_diff():
diff = "".join("+changed\n" for _ in range(850))
budget = Budget.for_review({}, diff)
assert budget.max_steps == 60
assert budget.max_total_tokens == 800_000
def test_explicit_budget_wins_over_diff_profile():
diff = "".join("+changed\n" for _ in range(2_100))
budget = Budget.for_review({"budget": {"max_steps": 9}}, diff)
assert budget.max_steps == 9
def test_budget_state_stops_at_token_limit():
state = BudgetState(Budget(max_steps=20, max_total_tokens=100))
assert state.record({"steps": 1, "total": 60, "output": 10}) == ""