aprover / bmc_agent /agents /refinement.py
theyoucheng's picture
Deploy AProver demo
ab54eb4 verified
Raw
History Blame Contribute Delete
4.23 kB
"""``RefinementAgent`` β€” migrates spec_refiner's LLM call.
When realism rejects a CEx as UNREALISTIC with an actionable
key_concern, this agent proposes the precise PRE clause to add so the
rejected witness state is excluded. The caller (``SpecRefiner``) is
responsible for the in-loop re-verification flow (apply clause β†’
re-run BMC β†’ check acceptance) β€” the agent itself just owns the
LLM-driven clause-proposal step.
This is C2 step 3 β€” same shape as FeedbackDistillAgent (single
structured-JSON call with custom max_tokens) but with refinement-
specific gating semantics handled by the orchestrating SpecRefiner.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Any, Optional
from bmc_agent.agents.base import BaseAgent
from bmc_agent.spec_refiner import (
RefinementProposal,
_REFINE_PROMPT,
_parse_refinement_response,
)
if TYPE_CHECKING:
from bmc_agent.cbmc import Counterexample
from bmc_agent.config import Config
from bmc_agent.llm import LLMClient
from bmc_agent.parser import FunctionInfo
from bmc_agent.realism_checker import RealismCheckResult
from bmc_agent.spec import Spec
class RefinementAgent(BaseAgent[RefinementProposal]):
"""Proposes a single PRE clause to tighten a function's spec so a
rejected CEx state is excluded.
Routing: ``BMC_AGENT_LLM_REFINEMENT_*`` env vars. The SpecRefiner
orchestrator handles gating (verdict + actionable key_concern) and
the post-refine BMC re-verification loop.
"""
name = "refinement"
def __init__(self, config: "Config", llm: "LLMClient") -> None:
from bmc_agent.prompts import SPEC_SYSTEM_PROMPT
self.system_prompt = SPEC_SYSTEM_PROMPT
super().__init__(config, llm)
def _llm_call_kwargs(self) -> dict:
# Per the original spec_refiner.propose_refinement β€” 4096 is
# plenty for a single-clause proposal, and thinking is off
# since the structured JSON already includes a rationale field.
return {"max_tokens": 4096, "thinking": False}
def build_prompt(
self,
*,
func_info: "FunctionInfo",
current_spec: "Spec",
rejected_cex: "Counterexample",
realism: "RealismCheckResult",
**_: Any,
) -> str:
sig = func_info.signature
params_str = ", ".join(
f"{t} {n}" for t, n in sig.parameters
) or "void"
fn_signature = f"{sig.return_type} {sig.name}({params_str})"
witness = "\n".join(
f" {k} = {v}"
for k, v in (rejected_cex.variable_assignments or {}).items()
)[:2000] or " (no witness state)"
# property_class extraction β€” last non-numeric segment of
# ``foo.pointer_dereference.5`` is the property class. Used by
# the evidence_tag suggestion in the prompt.
prop = rejected_cex.failing_property or ""
parts = prop.split(".")
prop_class = "unknown"
for p in reversed(parts):
if not p.isdigit():
prop_class = p
break
return _REFINE_PROMPT.format(
fn_name=func_info.name,
fn_signature=fn_signature,
fn_body=(func_info.body or "(unavailable)")[:4000],
pre_validity=current_spec.pre_validity or "(empty)",
pre_protocol=current_spec.pre_protocol or "(empty)",
postcondition=current_spec.postcondition or "(empty)",
failing_property=prop or "(unknown)",
witness_state=witness,
key_concern=realism.key_concern,
realism_reasoning=(realism.reasoning or "")[:1500],
property_class=prop_class,
)
def parse(self, response: str) -> Optional[RefinementProposal]:
if not response:
return None
# Reuse the existing parser β€” preserves identical behaviour for
# every JSON envelope variant (fenced, prose-embedded,
# unparseable). The parser returns RefinementProposal with
# scope="cannot-refine" when the LLM declined; that's still a
# valid parsed answer and is returned as-is.
return _parse_refinement_response(response, fn_name="<via-agent>")