Files
cleveragents-core/tools/_prefetch_section.py
T
drew dc4e9368f2 feat(auto-agents): block-store substrate + DRY refactors
Externalise large prompt sections (PR diff, comments, CI failure
logs, reviews, linked issues) into a cross-process SQLite-backed
block store so the worker can recover original content when an
intermediate tier-* agent's summarisation strips inline sections.

New substrate
-------------

- ``tools/_block_store.py`` — SQLite WAL, per-row 1MB cap, 1h TTL,
  janitor (startup + opportunistic per-hour in register()),
  threading.Lock around the periodic-sweep gate.
- ``tools/_block_prompt.py`` — registration glue + ``## Available
  blocks`` Markdown table renderer.
- ``tools/_prefetch_section.py`` — single-source-of-truth section
  registry; collapses the duplicate ``_register_*_blocks`` helpers
  the reviewer and implementer previously kept in lockstep.
- ``tools/mcp_block_store_server.py`` — FastMCP wrapper exposing
  ``block_fetch`` / ``block_list`` / ``block_register`` /
  ``block_invalidate`` to agents. Uses the expanded ``_mcp_common``
  helpers.
- ``tools/_implementer_escalation_helpers.py`` — pure helpers
  extracted from ``dispatch_implementer.py`` (~180 lines off the
  3284-line file); takes ``claim_runtime`` as a kwarg for clean DI.

DRY refactors
-------------

- ``tools/_backoff.py`` — shared ``Backoff`` dataclass collapses the
  three near-identical exponential-backoff state machines in
  ``_pr_comments_cache``, ``_ci_logs``, ``_pr_classification_cache``.
- ``tools/_mcp_common.py`` — expanded with ``bootstrap_loader``,
  ``error_envelope``, ``make_main`` so each MCP server's prelude is
  three lines.
- ``tools/_pr_diff.build_diff_section_full`` — returns a 4-tuple
  including the raw diff body so the reviewer's block-store
  registration reuses the bytes instead of doing a second HTTP fetch.

Wiring
------

- ``_review_prompt.build_review_prompt`` builds a ``PrefetchSection``
  registry via ``_review_sections``, registers them, and renders the
  ``## Available blocks`` table at the end of the prompt.
- ``_implementer_prefetch._fetch_pr_context`` /
  ``fetch_new_issue_context`` build the equivalent registry via
  ``_implementer_sections`` and stamp ``result.block_refs`` for the
  prompt builder to read.
- ``_implementer_prompt`` builders include
  ``_build_available_blocks_section(result)`` in all three flows.
- ``dispatch_review.main`` + ``dispatch_implementer.main`` call
  ``_block_store.janitor()`` at startup; the per-call opportunistic
  janitor in ``register()`` keeps the file bounded between restarts.

Agent contract updates
----------------------

- ``.opencode/agents/task-implementor.md`` +
  ``.opencode/agents/pr-review-worker.md``:
  - ``block_store*`` permission
  - new "Block-store substrate" paragraph explaining
    ``block_fetch`` / ``block_list`` as the summarisation recovery
    path.

Tests
-----

- ``test_block_store.py`` — 48 tests pinning every public contract
  (register/fetch/list/invalidate/janitor, key whitelist, TTL,
  size cap, WAL durability).
- ``test_block_prompt`` — covered transitively via the e2e test.
- ``test_block_store_recovery_e2e.py`` — builds a real prompt via
  ``build_pr_fix_prompt``, applies a heading-bounded summariser stub
  (``_summarise_inline_sections``), asserts inline content is stripped
  yet block keys survive and ``block_fetch`` recovers original content.
  Plus a ``block_list`` fallback test for the worst case where the
  table itself was summarised away.
- ``test_mcp_block_store_server.py`` — 27 wrapper-contract tests.
- ``test_mcp_block_store_transport.py`` — spawns the actual server
  subprocess via ``mcp.client.stdio`` and exercises the JSON-RPC
  transport round-trip in ~1.5s. Catches FastMCP schema /
  serialisation bugs the in-process tests miss.
- ``test_backoff.py`` — 14 behaviour-focused tests of the shared
  ``Backoff`` curve.
- ``test_pr_comments_cache.py`` + ``test_ci_logs.py`` — deleted the
  now-redundant ``TestComputeNextAttemptAfter`` / ``TestBackoffActive``
  / ``TestBackoffHelpers`` classes; ``test_backoff`` covers them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 22:55:52 -04:00

80 lines
2.6 KiB
Python

"""Single-source-of-truth registry for prefetched-section block writes.
Reviewer and implementer both register the same conceptual section
list (diff, comments, CI logs, reviews, ...). The two previous
``_try(...)`` helpers drifted; this module owns the iteration so
adding a new section is one edit, not two.
Scope is the block-store channel only. The on-disk sentinel
(:mod:`_pr_context_sentinel`) and the inline section renderers each
have schema/format needs that don't unify cleanly with the others
— folding them in would lose information, not save edits.
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Any
_TOOLS_DIR = str(Path(__file__).resolve().parent)
if _TOOLS_DIR not in sys.path:
sys.path.insert(0, _TOOLS_DIR)
from _loader import ( # noqa: E402 type: ignore[import-not-found]
load_sibling as _load_sibling,
)
_block_prompt = _load_sibling("_block_prompt", "_block_prompt.py")
@dataclass(frozen=True)
class PrefetchSection:
"""One row in the section registry.
``block_type`` becomes the second component of the canonical key
``pr-{N}-{block_type}-{sha}``. ``content`` is plain text (diff /
issue body) or JSON. Empty content is skipped by the registrar.
``description`` is the one-line ``## Available blocks`` cell.
"""
block_type: str
content: str
description: str
def register_sections(
sections: list[PrefetchSection],
*,
pr_number: int | None,
head_sha: str | None,
) -> list[Any]:
"""Register every non-empty section as a block. Returns the
list of :class:`_block_prompt.BlockRef` for sections that
registered successfully; rows where ``content`` was empty or
the block-store layer refused are silently skipped (the inline
section in the prompt is still the primary source).
The registrar never raises — every per-section failure is
swallowed by :func:`_block_prompt.register_section_block`,
which logs at WARN. Callers may wrap in a top-level try/except
if the substrate itself is in doubt, but no per-section
handling is needed.
"""
refs: list[Any] = []
for section in sections:
if not section.content:
continue
ref = _block_prompt.register_section_block(
pr_number=pr_number,
head_sha=head_sha,
block_type=section.block_type,
content=section.content,
description=section.description,
)
if ref is not None:
refs.append(ref)
return refs
__all__ = ("PrefetchSection", "register_sections")