1 Commits

Author SHA1 Message Date
Claude 592747d98d fix: resolve review budget feedback 2026-09-01 12:14:18 +00:00
3 changed files with 49 additions and 13 deletions
+2 -2
View File
@@ -163,6 +163,8 @@ def equivalent_cost(usage: dict, model: str, price_target: str = "") -> float:
"""Estimate comparison cost for one completed iteration."""
try:
from cost_model import PRICES, Usage, cost
except (ImportError, ModuleNotFoundError):
return 0.0
target = price_target or os.environ.get("PRAGENT_PRICE_TARGET", "claude-sonnet-5")
price = PRICES.get(target)
if price is None:
@@ -173,5 +175,3 @@ def equivalent_cost(usage: dict, model: str, price_target: str = "") -> float:
cache_writes=int(usage.get("cache_write") or 0),
output=int(usage.get("output") or 0),
), price)
except Exception:
return 0.0
+23 -1
View File
@@ -1,6 +1,7 @@
"""Isolated opencode process runtime."""
import os
import selectors
import subprocess
import time
@@ -130,6 +131,8 @@ def _run_process(
cmd, cwd=cwd, env=env, capture_output=True, text=True,
stdin=subprocess.DEVNULL, timeout=timeout,
)
if runner not in (None, subprocess.run):
raise ValueError("custom runners are unsupported for budgeted streaming")
existing_reason = budget_state.reason()
if existing_reason:
budget_state.cap_reason = existing_reason
@@ -142,9 +145,27 @@ def _run_process(
previous = {"steps": 0, "total": 0, "output": 0, "cost": 0.0}
cap_reason = ""
started = time.monotonic()
selector = selectors.DefaultSelector()
try:
assert proc.stdout is not None
for line in proc.stdout:
selector.register(proc.stdout, selectors.EVENT_READ)
while selector.get_map():
remaining = budget.max_duration_seconds - (time.monotonic() - started)
if remaining <= 0:
cap_reason = "max_duration_seconds"
budget_state.cap_reason = cap_reason
proc.terminate()
break
events = selector.select(timeout=remaining)
if not events:
cap_reason = "max_duration_seconds"
budget_state.cap_reason = cap_reason
proc.terminate()
break
line = proc.stdout.readline()
if not line:
selector.unregister(proc.stdout)
break
output.append(line)
_, usage = parse_events("".join(output))
if usage:
@@ -180,6 +201,7 @@ def _run_process(
proc.kill()
proc.wait()
finally:
selector.close()
if proc.stdout:
proc.stdout.close()
stderr = proc.stderr.read() if proc.stderr else ""
+15 -1
View File
@@ -3,6 +3,7 @@
import os
import sys
import json
import subprocess
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".."))
sys.path.insert(0, os.path.join(ROOT, "pilot"))
@@ -56,7 +57,20 @@ def test_process_terminates_after_step_budget():
proc = opencode_runtime._run_process(
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
timeout=10, parse_events=parse_opencode_events, budget=budget,
budget_state=state, model="glm-5.2:cloud", runner=None,
budget_state=state, model="glm-5.2:cloud", runner=subprocess.run,
)
assert state.snapshot()["cap_reason"] == "max_steps"
assert proc.stdout.count("step_finish") == 1
def test_process_terminates_silent_child_at_duration_budget():
code = "import time; time.sleep(30)"
budget = Budget(max_steps=20, max_duration_seconds=1)
state = BudgetState(budget)
proc = opencode_runtime._run_process(
[sys.executable, "-u", "-c", code], cwd=".", env=os.environ.copy(),
timeout=10, parse_events=parse_opencode_events, budget=budget,
budget_state=state, model="glm-5.2:cloud", runner=subprocess.run,
)
assert state.snapshot()["cap_reason"] == "max_duration_seconds"
assert proc.stdout == ""