refactor: split opencode runtime and tests
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
"""Parallel execution and selection of opencode review lenses."""
|
||||
|
||||
import concurrent.futures as _cf
|
||||
|
||||
from .opencode_synthesis import _normalize_lens_finding
|
||||
|
||||
LENS_TIMEOUT_S = 540
|
||||
|
||||
|
||||
def _run_one_lens(workdir, spec, model, factory_root):
|
||||
"""Run one lens through the compatibility module's runtime seam."""
|
||||
from . import opencode as oc
|
||||
|
||||
bin_ = oc._opencode_bin()
|
||||
home = oc._shared_home()
|
||||
oc._warm_opencode(home, model)
|
||||
env = oc._build_env(home)
|
||||
agent_path = spec.agent_path(factory_root)
|
||||
prompt = (
|
||||
f"You are the {spec.id} lens. Read .pragent/brief.md, load the "
|
||||
f"lens-orchestration skill (mandatory), and return STRICT JSON "
|
||||
f"findings per that skill. Cap at {spec.max_findings} findings, "
|
||||
f"severity >= {spec.severity_floor}. The agent markdown you should "
|
||||
f"load is at {agent_path} (it sets your role + permissions)."
|
||||
)
|
||||
cmd = [
|
||||
bin_, "run", "--pure", "--format", "json",
|
||||
"--agent", spec.id, "--dir", workdir, "--model", model, prompt,
|
||||
]
|
||||
try:
|
||||
proc = oc.subprocess.run(
|
||||
cmd, cwd=workdir, env=env, capture_output=True, text=True,
|
||||
stdin=oc.subprocess.DEVNULL, timeout=LENS_TIMEOUT_S,
|
||||
)
|
||||
except oc.subprocess.TimeoutExpired:
|
||||
print(f"pragent: lens {spec.id} timed out after {LENS_TIMEOUT_S}s", flush=True)
|
||||
return [], None, spec.id
|
||||
except Exception as e:
|
||||
print(f"pragent: lens {spec.id} crashed: {e}", flush=True)
|
||||
return [], None, spec.id
|
||||
|
||||
text, usage = oc.parse_opencode_events(proc.stdout or "")
|
||||
if not text.strip():
|
||||
print(
|
||||
f"pragent: lens {spec.id} empty text (rc={proc.returncode}); "
|
||||
f"stderr tail: {(proc.stderr or '')[-500:]}",
|
||||
flush=True,
|
||||
)
|
||||
return [], usage, spec.id
|
||||
|
||||
obj = oc._extract_json_object(text)
|
||||
if obj is None:
|
||||
print(f"pragent: lens {spec.id} produced no parseable JSON", flush=True)
|
||||
return [], usage, spec.id
|
||||
raw_findings = obj.get("findings") or []
|
||||
if not isinstance(raw_findings, list):
|
||||
return [], usage, spec.id
|
||||
|
||||
normalized = [
|
||||
finding
|
||||
for raw in raw_findings
|
||||
if (finding := _normalize_lens_finding(raw, spec, model)) is not None
|
||||
]
|
||||
print(
|
||||
f"pragent: lens {spec.id} findings={len(normalized)} "
|
||||
f"raw={len(raw_findings)} ok=1",
|
||||
flush=True,
|
||||
)
|
||||
return normalized, usage, spec.id
|
||||
|
||||
|
||||
def run_lenses(workdir, reviewers, default_model, factory_root):
|
||||
"""Run configured lenses in parallel and return results by lens id."""
|
||||
if not reviewers:
|
||||
return {}
|
||||
from . import opencode as oc
|
||||
|
||||
pool_size = min(len(reviewers), oc.MAX_PARALLEL_LENSES)
|
||||
out = {}
|
||||
with _cf.ThreadPoolExecutor(max_workers=pool_size) as ex:
|
||||
futures = {
|
||||
ex.submit(
|
||||
_run_one_lens, workdir, spec,
|
||||
spec.model or default_model, factory_root,
|
||||
): spec
|
||||
for spec in reviewers
|
||||
}
|
||||
for fut in _cf.as_completed(futures):
|
||||
spec = futures[fut]
|
||||
try:
|
||||
findings, usage, _ = fut.result()
|
||||
except Exception as e:
|
||||
print(f"pragent: lens {spec.id} worker crashed: {e}", flush=True)
|
||||
findings, usage = [], None
|
||||
out[spec.id] = (findings, usage)
|
||||
return out
|
||||
|
||||
|
||||
def intersect_with_triage(reviewers, selected_ids):
|
||||
"""Preserve reviewer order while applying the triage verdict."""
|
||||
if selected_ids is None:
|
||||
return list(reviewers)
|
||||
selected = set(selected_ids)
|
||||
return [reviewer for reviewer in reviewers if reviewer.id in selected]
|
||||
|
||||
|
||||
def filter_by_skip_if(reviewers, changed_paths):
|
||||
"""Drop lenses whose configured glob matches every changed path."""
|
||||
import fnmatch
|
||||
|
||||
out = []
|
||||
for reviewer in reviewers:
|
||||
pattern = reviewer.skip_if_all_changed_paths.strip()
|
||||
if pattern and changed_paths and all(
|
||||
fnmatch.fnmatch(path, pattern) for path in changed_paths
|
||||
):
|
||||
continue
|
||||
out.append(reviewer)
|
||||
return out
|
||||
|
||||
|
||||
def merge_usage(parts):
|
||||
"""Sum per-lens usage, retaining the existing usage dictionary shape."""
|
||||
from . import opencode as oc
|
||||
|
||||
base = oc._new_usage()
|
||||
base["duration_s"] = 0.0
|
||||
for usage in parts:
|
||||
if not usage:
|
||||
continue
|
||||
for key in base:
|
||||
if isinstance(base[key], (int, float)):
|
||||
base[key] += usage.get(key, 0) or 0
|
||||
return base
|
||||
|
||||
Reference in New Issue
Block a user