rein-aharness/rein_aharness/approaches.py

399 lines
12 KiB
Python

"""Approach registry: match ops_run → cheapest correct executor (REIN-A-0002-T02).
Prefer deterministic / structured llm-connect adapters before open-ended agent
sessions. Add rows in APPROACH_RULES (order = priority).
How to add a row:
1. Implement a ``run_*`` function or CLI command.
2. Append an ApproachRule with match predicates (labels / blob substrings).
3. Wire execution in ``execute_approach``.
4. Add pure match tests in ``tests/test_approaches.py``.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from rein_aharness.ops_run_client import OpsRun, OpsRunConfig, resolve_ops_target
from rein_aharness.taskspec import TaskSpecError
# Approach command names (stable; used in metrics + ops_run.result)
APPROACH_FI_RESEARCH_BRIEF = "fi-research-brief"
APPROACH_BRIEF_DAILY = "brief-daily"
APPROACH_BRIEF_WEEKLY = "brief-weekly"
APPROACH_MAIL_SCAN = "mail-scan"
APPROACH_MAIL_TRIAGE = "mail-triage"
APPROACH_MAIL_PIPELINE = "mail-scan+triage"
APPROACH_AGENT_SESSION = "agent-session"
APPROACH_UNMATCHED = "unmatched"
@dataclass(frozen=True)
class ApproachRule:
name: str
"""Match if any of these labels appear (case-insensitive)."""
labels_any: frozenset[str] = frozenset()
"""Match if all of these labels appear."""
labels_all: frozenset[str] = frozenset()
"""Match if any substring appears in definition id + labels + title + hint."""
blob_contains: frozenset[str] = frozenset()
"""If approach_hint equals this command name, select immediately."""
hint_name: str | None = None
# Ordered: first match wins. Cheapest / most specific first.
APPROACH_RULES: tuple[ApproachRule, ...] = (
ApproachRule(
name=APPROACH_FI_RESEARCH_BRIEF,
labels_any=frozenset({"research-brief", "freedom-intelligence"}),
blob_contains=frozenset(
{"fi-daily", "fi_daily", "fi-research", "freedom intelligence"}
),
hint_name=APPROACH_FI_RESEARCH_BRIEF,
),
ApproachRule(
name=APPROACH_BRIEF_WEEKLY,
labels_all=frozenset({"binky", "weekly-review"}),
blob_contains=frozenset(
{"binky-weekly-review", "binky_weekly_review", "weekly founder review"}
),
hint_name=APPROACH_BRIEF_WEEKLY,
),
ApproachRule(
name=APPROACH_BRIEF_DAILY,
labels_any=frozenset({"rhythm", "binky-daily", "daily-brief"}),
labels_all=frozenset(), # rhythm alone is enough for Binky daily
blob_contains=frozenset(
{"binky-daily", "binky_daily", "daily-rhythm", "daily_brief"}
),
hint_name=APPROACH_BRIEF_DAILY,
),
ApproachRule(
name=APPROACH_MAIL_PIPELINE,
labels_any=frozenset({"mail-intake", "mail_intake"}),
blob_contains=frozenset({"mail-intake", "mail_intake", "weekly-mail"}),
hint_name=APPROACH_MAIL_PIPELINE,
),
ApproachRule(
name=APPROACH_MAIL_SCAN,
labels_any=frozenset({"mail-scan"}),
hint_name=APPROACH_MAIL_SCAN,
),
ApproachRule(
name=APPROACH_MAIL_TRIAGE,
labels_any=frozenset({"mail-triage"}),
hint_name=APPROACH_MAIL_TRIAGE,
),
ApproachRule(
name=APPROACH_AGENT_SESSION,
labels_any=frozenset({"agent-session", "agent_session"}),
blob_contains=frozenset({"agent-session"}),
hint_name=APPROACH_AGENT_SESSION,
),
)
def _match_blob(run: OpsRun) -> str:
parts = [
run.activity_definition_id or "",
run.title or "",
run.approach_hint or "",
" ".join(run.labels or []),
run.source_id or "",
run.target_repo or "",
]
return " ".join(parts).lower()
def select_approach(run: OpsRun) -> str:
"""Return approach command name for this ops_run (pure)."""
hint = (run.approach_hint or "").strip().lower()
if hint:
for rule in APPROACH_RULES:
if rule.hint_name and hint in {
rule.hint_name,
rule.name,
rule.hint_name.replace("-", "_"),
}:
return rule.name
# Explicit known command as hint
known = {
APPROACH_FI_RESEARCH_BRIEF,
APPROACH_BRIEF_DAILY,
APPROACH_BRIEF_WEEKLY,
APPROACH_MAIL_SCAN,
APPROACH_MAIL_TRIAGE,
APPROACH_MAIL_PIPELINE,
APPROACH_AGENT_SESSION,
}
if hint in known or hint.replace("_", "-") in known:
return hint.replace("_", "-")
labels = {str(x).lower() for x in (run.labels or [])}
blob = _match_blob(run)
for rule in APPROACH_RULES:
matches_all = bool(rule.labels_all) and rule.labels_all.issubset(labels)
matches_any = bool(rule.labels_any) and bool(rule.labels_any & labels)
matches_blob = bool(rule.blob_contains) and any(
substring in blob for substring in rule.blob_contains
)
if matches_all or matches_any or matches_blob:
return rule.name
return APPROACH_UNMATCHED
@dataclass
class ApproachResult:
ok: bool
approach: str
result: dict[str, Any] = field(default_factory=dict)
reason: str = ""
reopen: bool = False
def execute_approach(
run: OpsRun,
*,
approach: str | None = None,
config: OpsRunConfig | None = None,
report_to_hub: bool = True,
commit: bool = True,
) -> ApproachResult:
"""Dispatch to the selected executor. Never raises for business failure."""
cfg = config or OpsRunConfig.from_env()
name = approach or select_approach(run)
if name == APPROACH_UNMATCHED:
return ApproachResult(
ok=False,
approach=name,
reason=(
"no approach matched labels/definition; "
f"labels={run.labels!r} def={run.activity_definition_id!r}"
),
reopen=False,
)
try:
target = resolve_ops_target(run, cfg)
except TaskSpecError as exc:
return ApproachResult(
ok=False,
approach=name,
reason=str(exc),
reopen=False,
)
try:
if name == APPROACH_FI_RESEARCH_BRIEF:
return _run_fi(target, report_to_hub=report_to_hub, commit=commit)
if name == APPROACH_BRIEF_DAILY:
return _run_brief_daily(target, report_to_hub=report_to_hub, commit=commit)
if name == APPROACH_BRIEF_WEEKLY:
return _run_brief_weekly(target, report_to_hub=report_to_hub, commit=commit)
if name == APPROACH_MAIL_SCAN:
return _run_mail_scan(target, report_to_hub=report_to_hub)
if name == APPROACH_MAIL_TRIAGE:
return _run_mail_triage(target, report_to_hub=report_to_hub, commit=commit)
if name == APPROACH_MAIL_PIPELINE:
return _run_mail_pipeline(
target, report_to_hub=report_to_hub, commit=commit
)
if name == APPROACH_AGENT_SESSION:
return _run_agent_session(run, target, report_to_hub=report_to_hub)
except Exception as exc: # noqa: BLE001 — surface as fail ops_run
return ApproachResult(
ok=False,
approach=name,
reason=f"{type(exc).__name__}: {exc}",
reopen=True,
)
return ApproachResult(
ok=False,
approach=name,
reason=f"approach not implemented: {name}",
reopen=False,
)
def _run_fi(target: Path, *, report_to_hub: bool, commit: bool) -> ApproachResult:
from rein_aharness.fi_research_brief import run_fi_research_brief
r = run_fi_research_brief(
target_repo=target,
report_to_hub=report_to_hub,
commit=commit,
)
return ApproachResult(
ok=r.ok,
approach=APPROACH_FI_RESEARCH_BRIEF,
result={
"date": r.date,
"path": r.path,
"wrote": r.wrote,
"committed": r.committed,
"skipped_existing": r.skipped_existing,
"collection_candidates": r.collection_candidates,
"head_after": r.head_after,
"target_repo": "freedom-intelligence",
},
reason=r.reason,
reopen=not r.ok and not r.skipped_existing,
)
def _run_brief_daily(
target: Path, *, report_to_hub: bool, commit: bool
) -> ApproachResult:
from rein_aharness.brief_daily import run_brief_daily
r = run_brief_daily(
target_repo=target,
report_to_hub=report_to_hub,
commit=commit,
)
return ApproachResult(
ok=r.ok,
approach=APPROACH_BRIEF_DAILY,
result={
"date": r.date,
"path": r.path,
"wrote": r.wrote,
"committed": r.committed,
"skipped_existing": r.skipped_existing,
"head_after": r.head_after,
"target_repo": "binky-control",
},
reason=r.reason,
reopen=not r.ok and not r.skipped_existing,
)
def _run_brief_weekly(
target: Path, *, report_to_hub: bool, commit: bool
) -> ApproachResult:
from rein_aharness.brief_weekly import run_brief_weekly
r = run_brief_weekly(
target_repo=target,
report_to_hub=report_to_hub,
commit=commit,
)
return ApproachResult(
ok=r.ok,
approach=APPROACH_BRIEF_WEEKLY,
result={
"date": r.date,
"path": r.path,
"wrote": r.wrote,
"committed": r.committed,
"skipped_existing": r.skipped_existing,
"milestone_moved": r.milestone_moved,
"risk005_state": r.risk005_state,
"head_after": r.head_after,
"target_repo": "binky-control",
},
reason=r.reason,
reopen=not r.ok and not r.skipped_existing,
)
def _run_mail_scan(target: Path, *, report_to_hub: bool) -> ApproachResult:
from rein_aharness.mailscan import run_mail_scan
r = run_mail_scan(target_repo=target, report_to_hub=report_to_hub)
return ApproachResult(
ok=r.ok,
approach=APPROACH_MAIL_SCAN,
result={
"report": r.report_path,
"new_messages": r.new_messages,
"auth_lane": r.auth_lane,
},
reason=r.reason,
reopen=not r.ok,
)
def _run_mail_triage(
target: Path, *, report_to_hub: bool, commit: bool
) -> ApproachResult:
from rein_aharness.mail_triage import run_mail_triage
r = run_mail_triage(
target_repo=target,
report_to_hub=report_to_hub,
commit=commit,
)
return ApproachResult(
ok=r.ok,
approach=APPROACH_MAIL_TRIAGE,
result={
"report": r.report,
"entries_applied": r.entries_applied,
"committed": r.committed,
"head_after": r.head_after,
},
reason=r.reason,
reopen=not r.ok,
)
def _run_mail_pipeline(
target: Path, *, report_to_hub: bool, commit: bool
) -> ApproachResult:
scan = _run_mail_scan(target, report_to_hub=report_to_hub)
if not scan.ok:
scan.approach = APPROACH_MAIL_PIPELINE
return scan
triage = _run_mail_triage(target, report_to_hub=report_to_hub, commit=commit)
return ApproachResult(
ok=triage.ok,
approach=APPROACH_MAIL_PIPELINE,
result={"scan": scan.result, "triage": triage.result},
reason=triage.reason or scan.reason,
reopen=not triage.ok,
)
def _run_agent_session(
run: OpsRun, target: Path, *, report_to_hub: bool
) -> ApproachResult:
from rein_aharness.ops_run_client import ops_run_to_taskspec
from rein_aharness.runner import run_task
# Infer agent/event from labels (reuse intake hints)
from rein_aharness.intake import EmittedIssue, infer_agent_and_event
pseudo = EmittedIssue(
issue_id=run.id,
title=run.title,
description=run.description,
labels=list(run.labels),
target_repo=run.target_repo,
activity_definition_id=run.activity_definition_id,
)
agent, event = infer_agent_and_event(pseudo)
spec = ops_run_to_taskspec(
run,
agent=agent,
completion_event_type=event,
)
# target already resolved into TaskSpec
assert spec.target_repo == target or True
r = run_task(spec, report_to_hub=report_to_hub)
return ApproachResult(
ok=r.ok,
approach=APPROACH_AGENT_SESSION,
result={
"committed": r.committed,
"head_after": r.head_after,
"tool_profile": r.tool_profile,
"tokens_spent": r.tokens_spent,
},
reason=r.reason,
reopen=not r.ok,
)