feat: adapt review budget to diff size
This commit is contained in:
@@ -157,6 +157,11 @@ overridden deployment-wide with `PRAGENT_MAX_REVIEW_STEPS`,
|
|||||||
`PRAGENT_REVIEW_TIMEOUT`, or per repository in the trusted base-branch
|
`PRAGENT_REVIEW_TIMEOUT`, or per repository in the trusted base-branch
|
||||||
`.pr-review.json`; repository values win.
|
`.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,
|
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
|
~2.1M input tokens each, **zero cache reads or writes**. The demo repo's PR, same
|
||||||
tier: 126K tokens.
|
tier: 126K tokens.
|
||||||
|
|||||||
@@ -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
|
review body and Langfuse metadata. Environment variables provide deployment-wide
|
||||||
defaults; repository budget values override them.
|
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
|
**Cross-lens dedup:** synthesiser drops duplicates by
|
||||||
`sha256[:16](path|line|severity|problem[:80])` (matches the feedback DB's
|
`sha256[:16](path|line|severity|problem[:80])` (matches the feedback DB's
|
||||||
`posthash`), then promotes multi-lens agreement by one severity step
|
`posthash`), then promotes multi-lens agreement by one severity step
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ DEFAULTS = {
|
|||||||
"max_duration_seconds": 480,
|
"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)
|
@dataclass(frozen=True)
|
||||||
class Budget:
|
class Budget:
|
||||||
@@ -63,6 +70,34 @@ class Budget:
|
|||||||
price_target=price_target,
|
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:
|
def _env_int(name: str, default: int) -> int:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -440,7 +440,7 @@ def run_lenses_review(
|
|||||||
always has: prose + a final ```json fence with the legacy schema).
|
always has: prose + a final ```json fence with the legacy schema).
|
||||||
"""
|
"""
|
||||||
os.makedirs(WORK_ROOT, exist_ok=True)
|
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)
|
budget_state = budget_state or BudgetState(budget)
|
||||||
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
|
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
|
||||||
keep = bool(os.environ.get("PRAGENT_KEEP_WORK"))
|
keep = bool(os.environ.get("PRAGENT_KEEP_WORK"))
|
||||||
@@ -687,7 +687,7 @@ def run(
|
|||||||
additional_context=additional_context,
|
additional_context=additional_context,
|
||||||
)
|
)
|
||||||
|
|
||||||
budget = Budget.from_config(config)
|
budget = Budget.for_review(config, diff)
|
||||||
budget_state = BudgetState(budget)
|
budget_state = BudgetState(budget)
|
||||||
os.makedirs(WORK_ROOT, exist_ok=True)
|
os.makedirs(WORK_ROOT, exist_ok=True)
|
||||||
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
|
workdir = tempfile.mkdtemp(prefix=f"{repo.replace('/', '_')}-{sha[:8]}-", dir=WORK_ROOT)
|
||||||
|
|||||||
@@ -18,6 +18,19 @@ def test_budget_reads_config_over_environment(monkeypatch):
|
|||||||
assert budget.max_steps == 7
|
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():
|
def test_budget_state_stops_at_token_limit():
|
||||||
state = BudgetState(Budget(max_steps=20, max_total_tokens=100))
|
state = BudgetState(Budget(max_steps=20, max_total_tokens=100))
|
||||||
assert state.record({"steps": 1, "total": 60, "output": 10}) == ""
|
assert state.record({"steps": 1, "total": 60, "output": 10}) == ""
|
||||||
|
|||||||
Reference in New Issue
Block a user