open-swe/agent/tools/save_review_style.py
Johannes du Plessis 209132d355
refactor: durable interrupt dispatch + completion webhook (#1621)
* wip(rebuild): core reliability spine

- remove PR-babysitting (ci_autofix + ci_monitor graph + webhook wiring)
- dispatch core: agent/dispatch.py with multitask_strategy=interrupt +
  durability=sync + completion webhook; reroute all webhook + plan triggers;
  drop the racy in-process lock + is_thread_active busy-check
- completion webhook: agent/completion.py + /webhooks/run-complete loopback
  route for failure/timeout replies (idempotent)

Co-authored-by: open-swe[bot]

* feat(rebuild): async tools, reconcile, shared http timeouts, assembly tuning

Parallel batch on top of the reliability spine:
- async-ify all 24 tools (drop asyncio.run; requests->httpx); re-implement the
  http_request/fetch_url SSRF + DNS-rebinding defense httpx-natively and harden
  the IP check to 'not is_global' (+ IPv4-mapped unwrap)
- reconcile.py: stale pending-run sweep (threads.search -> per-thread runs.list
  -> cancel_many), wired into the scheduler graph via task='reconcile'
- shared DEFAULT_HTTP_TIMEOUT (agent/utils/http.py) on every bare
  httpx.AsyncClient() across utils/dashboard/webapp/middleware
- run budget: MODEL_CALL_RECURSION_LIMIT 5000->250
- fix stale OpenAI->Anthropic fallback id (claude-opus-4-5 -> 4-8)
- drop redundant custom repair middleware (deepagents auto-adds PatchToolCalls)
- confirm tool-result eviction + summarization auto-wired via backend
- slim system prompt ~8% (full harness-profile rewrite deferred)

Co-authored-by: open-swe[bot]

* feat(rebuild): harness-profile prompt + split webhooks out of webapp

- prompt.py: own the system prompt via a registered harness profile
  (OPEN_SWE_SHARED_BASE, kept neutral so the read-only reviewer/analyzer that
  share it stay safe), registered across all 4 providers; per-thread values
  stay in construct_system_prompt. Assembled main-agent prompt ~6.8k -> ~3.1k
  tokens (~55% smaller); de-duped PR/commit/suite/force-push guidance; dropped
  ALL-CAPS markers.
- webapp.py 3325 -> 1890 LOC: moved 14 per-source handlers into
  agent/webhooks/{linear,slack,github}.py; webapp re-exports them for the
  routes + tests; moved handlers reach shared helpers via the webapp namespace
  to preserve the test suite's monkeypatch targets.

Full suite: 1168 passing, lint clean.

Co-authored-by: open-swe[bot]

* Restore MODEL_CALL_RECURSION_LIMIT to 5000 for long-running tasks

Reverts the 250 cap from the run-budget change — long-running tasks legitimately
need many model calls. The notify_step_limit_reached safety net still fires if a
run does hit the cap, so runs end with a signal either way.

Co-authored-by: open-swe[bot]

* fix: address PR review (auth, SSRF, interrupted status, redirect headers)

- completion.py: drop `interrupted` from failure statuses — with
  multitask_strategy=interrupt a follow-up ends the prior run as interrupted,
  which is healthy, not a failure to report. [open-swe]
- /webhooks/run-complete: shared-secret auth — dispatch appends ?token= when
  RUN_COMPLETE_WEBHOOK_SECRET is set; route verifies via hmac.compare_digest.
  [corridor-security]
- SSRF: extract the URL validator to agent/utils/url_safety.py and apply it
  before server-side image fetches in multimodal.fetch_image_block.
  [corridor-security]
- http_request: preserve caller headers/extensions across redirect hops instead
  of dropping them on the first hop. [open-swe]

Co-authored-by: open-swe[bot]

* chore: remove REBUILD_PLAN.md (planning doc, not needed in the repo)

Co-authored-by: open-swe[bot]

* fix: fail closed on run-complete webhook auth when secret unset

Corridor follow-up: verify_run_complete_token returns False (not True) when
RUN_COMPLETE_WEBHOOK_SECRET is unset, so the public route is never
unauthenticated. Logs a startup warning when the secret is absent, and dispatch
skips registering the webhook when there's no secret (no rejected callbacks).

Co-authored-by: open-swe[bot]

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
2026-06-26 13:48:38 -07:00

68 lines
2.6 KiB
Python

"""Tool: persist synthesized per-repo review style prompt."""
from __future__ import annotations
import logging
from typing import Any
from langgraph.config import get_config
from ..dashboard.analyzer_cron import ensure_continual_cron
from ..dashboard.review_styles import mark_analysis_completed, mark_analysis_failed
logger = logging.getLogger(__name__)
async def _complete_and_register(full_name: str, **completed_kwargs: Any) -> dict[str, Any]:
"""Persist the prompt, then ensure the repo's nightly continual cron exists.
Cron registration is idempotent, so continual runs completing later don't
re-register it; it just guarantees a cron once a prompt first exists.
"""
record = await mark_analysis_completed(full_name, **completed_kwargs)
try:
await ensure_continual_cron(full_name)
except Exception:
logger.exception("Failed to ensure continual cron for %s", full_name)
return record
async def save_review_style_prompt(
custom_prompt: str,
analysis_summary: str = "",
top_reviewers: str = "",
prs_sampled: int = 0,
reviews_sampled: int = 0,
) -> dict[str, Any]:
"""Save the synthesized repository-specific review style prompt.
Call this once at the end of style analysis with the final prompt text
that should be injected into the reviewer agent for this repository.
"""
config = get_config()
configurable = config.get("configurable") or {}
full_name = configurable.get("review_style_full_name")
if not isinstance(full_name, str) or "/" not in full_name:
return {"ok": False, "error": "review_style_full_name missing from config"}
reviewers_from_args = [r.strip() for r in top_reviewers.split(",") if r.strip()]
reviewers_from_config = configurable.get("review_style_top_reviewers") or []
merged_reviewers = reviewers_from_args or (
list(reviewers_from_config) if isinstance(reviewers_from_config, list) else []
)
prs_count = prs_sampled or int(configurable.get("review_style_prs_sampled") or 0)
reviews_count = reviews_sampled or int(configurable.get("review_style_reviews_sampled") or 0)
if not custom_prompt.strip():
await mark_analysis_failed(full_name, "custom_prompt was empty")
return {"ok": False, "error": "custom_prompt cannot be empty"}
record = await _complete_and_register(
full_name,
custom_prompt=custom_prompt.strip(),
analysis_summary=analysis_summary.strip(),
top_reviewers=merged_reviewers,
prs_sampled=prs_count,
reviews_sampled=reviews_count,
)
return {"ok": True, "full_name": full_name, "status": record.get("status")}