234 lines
12 KiB
Python
234 lines
12 KiB
Python
# SPDX-FileCopyrightText: 2026 Art Sea LLC
|
|
#
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
|
|
import json
|
|
import logging
|
|
from typing import cast
|
|
|
|
from band_aider.ai_client.completion_client import CompletionClient
|
|
from band_aider.ai_client.json_completion import load_json_completion
|
|
from band_aider.context.class_meta import ClassMeta
|
|
from band_aider.context.validation_utils import (
|
|
ValidationError,
|
|
ValidationLocation,
|
|
ValidationResult,
|
|
SemanticValidationError,
|
|
validate_shape,
|
|
)
|
|
from band_aider.judgement.edit_request import AiderEditRequest
|
|
from band_aider.judgement.code_based_reasoning import CodeBasedReasoning
|
|
from band_aider.judgement.contextual_file_renderer import ContextualFileRenderer
|
|
from band_aider.judgement.fidelity import Fidelity
|
|
from band_aider.judgement.file_manager import FileManager
|
|
from band_aider.judgement.stabilization_decision import StabilizationDecision
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
|
class BootstrapAcceptanceCheck(StabilizationDecision):
|
|
"""Judge bootstrap edits; ``check()`` admits either verdict only after its source citations pass."""
|
|
|
|
_VALID_RESPONSE_REFERENCES: list[object] = [
|
|
{
|
|
"outcome": "pass",
|
|
"reason": ["return result"],
|
|
"instruction": "",
|
|
},
|
|
{
|
|
"outcome": "fail",
|
|
"reason": ["return old_api()"],
|
|
"instruction": "concrete correction for the current retry",
|
|
},
|
|
]
|
|
|
|
@property
|
|
def valid_response_references(self) -> list[object]:
|
|
"""Return every JSON reference accepted by bootstrap edit acceptance."""
|
|
|
|
return self._VALID_RESPONSE_REFERENCES
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
client: CompletionClient,
|
|
file_manager: FileManager,
|
|
edit_request: AiderEditRequest,
|
|
editable_sources: list[ClassMeta],
|
|
readonly_sources: list[ClassMeta],
|
|
readonly_tests: list[ClassMeta],
|
|
previous_summary: str,
|
|
baseline: Mapping[str, str] | None = None,
|
|
) -> None:
|
|
"""Initialize one bootstrap source-edit acceptance judgement."""
|
|
|
|
super().__init__(
|
|
client=client,
|
|
file_manager=file_manager,
|
|
request=edit_request,
|
|
logger=_LOGGER,
|
|
)
|
|
self._reason_check = CodeBasedReasoning(
|
|
file_manager=file_manager, files=editable_sources, logger=_LOGGER,
|
|
baseline=baseline,
|
|
)
|
|
renderer = ContextualFileRenderer.build(
|
|
file_manager=file_manager,
|
|
client=client,
|
|
editable_files=editable_sources,
|
|
readonly_files=[*readonly_sources, *readonly_tests],
|
|
prompt=edit_request.authoritative_prompt,
|
|
pinned_context=edit_request.pinned_context,
|
|
backlog_guidance=edit_request.guidance,
|
|
failure_error_message=edit_request.failure_error_message,
|
|
failure_stack_trace=edit_request.failure_stack_trace,
|
|
failure_raw_output=edit_request.failure_raw_output,
|
|
logger=_LOGGER.getChild("contextual-file-renderer"),
|
|
prompt_formatter_builder=self._prompt_formatter_builder,
|
|
)
|
|
section = self._prompt_formatter_builder.generate
|
|
request_body = lambda: "\n\n".join((
|
|
section('AUTHORITATIVE PROMPT', escape=True)(edit_request.authoritative_prompt),
|
|
section('EDITABLE FILES', escape=True)("\n".join(map(str, edit_request.editable_files))),
|
|
section('READONLY FILES', escape=True)("\n".join(map(str, edit_request.readonly_files))),
|
|
section('PINNED CONTEXT', escape=True)("\n".join(map(str, edit_request.pinned_context))),
|
|
section('BACKLOG GUIDANCE', escape=True)("\n".join(map(str, edit_request.guidance))),
|
|
section('FAILURE ERROR MESSAGE', escape=True)(edit_request.failure_error_message),
|
|
section('FAILURE STACK TRACE', escape=True)("\n".join(map(str, edit_request.failure_stack_trace))),
|
|
section('FAILURE RAW OUTPUT', escape=True)(edit_request.failure_raw_output),
|
|
section('RUN INFORMATION', escape=True)(edit_request.failure_run_info),
|
|
section('ALLOWED ADDITIONAL ROOTS', escape=True)(
|
|
"\n".join(map(str, edit_request.allowed_additional_roots))
|
|
),
|
|
))
|
|
editable_body = lambda: "\n\n".join(map(
|
|
section('SOURCE'),
|
|
map(lambda source: lambda: "\n\n".join((
|
|
section('LOCATION', escape=True)(source.location),
|
|
section('SUMMARY', escape=True)(source.summary),
|
|
section('RELEVANCE', escape=True)("\n".join(map(str, source.relevance))),
|
|
section('RELEVANT SYMBOLS', escape=True)(
|
|
"\n".join(map(lambda entry: f"{entry[0]}: {entry[1]}", source.relevant_symbols.items()))
|
|
),
|
|
section('CURRENT EVIDENCE', escape=True)(renderer.read(source, Fidelity.DIFF)),
|
|
)), editable_sources),
|
|
))
|
|
readonly_source_body = lambda: "\n\n".join(map(
|
|
section('SOURCE'),
|
|
map(lambda source: lambda: "\n\n".join((
|
|
section('LOCATION', escape=True)(source.location),
|
|
section('SUMMARY', escape=True)(source.summary),
|
|
section('RELEVANCE', escape=True)("\n".join(map(str, source.relevance))),
|
|
section('RELEVANT SYMBOLS', escape=True)(
|
|
"\n".join(map(lambda entry: f"{entry[0]}: {entry[1]}", source.relevant_symbols.items()))
|
|
),
|
|
section('CURRENT EVIDENCE', escape=True)(renderer.read(source, Fidelity.DIFF)),
|
|
)), readonly_sources),
|
|
))
|
|
readonly_test_body = lambda: "\n\n".join(map(
|
|
section('TEST'),
|
|
map(lambda test: lambda: "\n\n".join((
|
|
section('LOCATION', escape=True)(test.location),
|
|
section('SUMMARY', escape=True)(test.summary),
|
|
section('RELEVANCE', escape=True)("\n".join(map(str, test.relevance))),
|
|
section('RELEVANT SYMBOLS', escape=True)(
|
|
"\n".join(map(lambda entry: f"{entry[0]}: {entry[1]}", test.relevant_symbols.items()))
|
|
),
|
|
section('CURRENT EVIDENCE', escape=True)(renderer.read(test, Fidelity.DIFF)),
|
|
)), readonly_tests),
|
|
))
|
|
prompt = lambda: (
|
|
"ACCEPTANCE ROLE\n"
|
|
"You are judging one completed bootstrap source edit attempt before stabilization has an original CoT.\n"
|
|
"Judge the edit against the bootstrap assignment and source-edit boundaries in the original edit request.\n\n"
|
|
f"{section('ORIGINAL AIDER EDIT REQUEST')(request_body)}\n\n"
|
|
f"{section('EDITABLE SOURCE CONTEXT')(editable_body)}\n\n"
|
|
f"{section('READONLY SOURCE CONTEXT, NOT EDITABLE')(readonly_source_body)}\n\n"
|
|
f"{section('READONLY TEST CONTEXT, NOT EDITABLE')(readonly_test_body)}\n\n"
|
|
f"{section('DEPENDENCY REFERENCE CARDS', escape=True)(renderer.get_reference_card_evidence())}\n\n"
|
|
f"{section('EDITOR SUMMARY FOR THIS ATTEMPT', escape=True)(previous_summary)}\n\n"
|
|
"DECISION RULE\n"
|
|
f"{self._reason_check.render_baseline()}\n\n"
|
|
"For ordinary citations, reason must be a nonempty array of all exact current editable-source lines relied on for every part of the judgment, including relevant conditions, operations, and results; one complete nonblank line per item, without line numbers or diff markers.\n"
|
|
"Every ordinary item must match current editable source: items are a conjunction, so one invalid item rejects the complete response; readonly evidence cannot satisfy this check.\n"
|
|
"This static check verifies supplied lines only; you must judge relevance and include all supporting evidence for either pass or fail.\n"
|
|
"Return strict JSON only. Allowed internal outcome values are exactly \"pass\" or \"fail\". Return pass only if the diff addresses the original bootstrap request, leaves nothing out, adds nothing unrelated, and has no obvious accidental deletion, blank implementation, placeholder implementation, test edit, or compatibility break. Empty diff may pass only when it satisfies the exact request. For fail, the instruction must direct the next attempt from the supplied current source state.\n\n"
|
|
"PASS SHAPE\n"
|
|
f"{json.dumps(self.valid_response_references[0], separators=(',', ':'))}\n\n"
|
|
"FAIL SHAPE\n"
|
|
f"{json.dumps(self.valid_response_references[1], separators=(',', ':'))}"
|
|
)
|
|
self.prompt = section("BOOTSTRAP EDIT ACCEPTANCE")(prompt)
|
|
|
|
def _decide(
|
|
self,
|
|
response: object | None = None,
|
|
validation: ValidationResult | None = None,
|
|
) -> tuple[object | None, ValidationResult]:
|
|
"""Request, validate, and record one Bootstrap acceptance move."""
|
|
|
|
# Add only the preceding rejection when this is a correction attempt.
|
|
# Keep the original request and only the latest rejected response in each correction.
|
|
if validation is None:
|
|
prompt = self.prompt
|
|
else:
|
|
self.logger.warning("Bootstrap acceptance retrying response=%s errors=%s", self.escalation.last_response, validation.stringify())
|
|
prompt = super()._format_prompt_w_feedback(validation) + (
|
|
"\nReassess the whole bootstrap judgment and return a complete replacement reason array "
|
|
"retaining all supporting lines still relied on, not just corrected items. Every item must pass."
|
|
)
|
|
root = ValidationLocation([])
|
|
try:
|
|
data = load_json_completion(
|
|
self.escalation,
|
|
self.render_prompt_for_turn(prompt),
|
|
prompt_formatter_builder=self._prompt_formatter_builder,
|
|
)
|
|
except json.JSONDecodeError as error:
|
|
return error.doc, ValidationResult(errors=(ValidationError(
|
|
location=root,
|
|
error=str(error),
|
|
),))
|
|
|
|
validation = validate_shape(
|
|
data,
|
|
self.valid_response_references,
|
|
location=root,
|
|
)
|
|
# Keep malformed acceptance moves out of Bootstrap's edit transition.
|
|
if not validation.valid:
|
|
return data, validation
|
|
response = cast("dict[str, object]", data)
|
|
outcome = cast(str, response["outcome"])
|
|
reason = cast(list[str], response["reason"])
|
|
instruction = cast(str, response["instruction"])
|
|
error = ""
|
|
# Require one supported acceptance outcome before enforcing its instruction rule.
|
|
if outcome not in {"pass", "fail"}:
|
|
error = "outcome must be pass or fail."
|
|
elif outcome == "pass" and instruction.strip():
|
|
error = "pass must leave instruction empty."
|
|
elif outcome == "fail" and not instruction.strip():
|
|
error = "fail must include a correction instruction."
|
|
# Return semantic feedback to the base-owned retry without changing decision state.
|
|
if error:
|
|
return data, ValidationResult(errors=(SemanticValidationError(
|
|
location=root,
|
|
error=error,
|
|
),))
|
|
|
|
# Validate the response array before recording either verdict or formatting its summary.
|
|
if not self._reason_check(reason):
|
|
return data, self._reason_check.validation
|
|
|
|
self.outcome = outcome
|
|
self.reason = "\n".join(reason)
|
|
self.additional = {"instruction": instruction.strip()}
|
|
passed = self.outcome == "pass"
|
|
self.changes.append((self, passed))
|
|
self.logger.info("Bootstrap acceptance validated outcome=%s reason=%s", outcome, reason)
|
|
return data, ValidationResult()
|