Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions tests/test_state_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,3 +664,13 @@ def test_diagnosis_confidence_weak_without_file_evidence(tmp_path: Path) -> None
}
confidence = state_runtime.classify_diagnosis_target_confidence(runner, diagnosis, failure_evidence=None)
assert confidence == "weak"

def test_response_commits_to_code_edit_detects_concrete_intent() -> None:
assert state_runtime.response_commits_to_code_edit("I'll update x.py to handle empty values.")
assert state_runtime.response_commits_to_code_edit("The fix is to add this handler to the registry.")


def test_response_commits_to_code_edit_ignores_suggestions_or_non_edits() -> None:
assert not state_runtime.response_commits_to_code_edit("One possible fix would be to adjust the parser.")
assert not state_runtime.response_commits_to_code_edit("You could update the default value.")
assert not state_runtime.response_commits_to_code_edit("No code changes are needed.")
31 changes: 31 additions & 0 deletions villani_code/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,7 @@ def __init__(
self._patch_sanity_retry_pending = False
self._first_attempt_write_lock_active = False
self._first_attempt_locked_target = ""
self._edit_intent_nudge_used = False
self._context_governance = ContextGovernanceManager(self.repo)
self._planning_read_only = False
self._runtime_mode: Literal["execution", "planning"] = "execution"
Expand Down Expand Up @@ -1329,6 +1330,31 @@ def _budget_reason(
self.event_callback({"type": "benchmark_scope_reminder_injected", "task_id": self.benchmark_config.task_id, "reason": "no_meaningful_edit"})
messages.append({"role": "user", "content": [{"type": "text", "text": reminder}]})
continue
if (
not self._planning_read_only
and not self._edit_intent_nudge_used
and self._response_commits_to_code_edit(
" ".join(
block.get("text", "")
for block in response.get("content", [])
if isinstance(block, dict) and block.get("type") == "text"
)
)
):
self._edit_intent_nudge_used = True
messages.append(
{
"role": "user",
"content": [
{
"type": "text",
"text": "You described a concrete code change, but no file was modified. Apply the minimal edit now, then verify if possible.",
}
],
}
)
self.event_callback({"type": "edit_intent_without_edit_nudged"})
continue
reason = _budget_reason(completed=True)
if reason:
return _finish_bounded(response, reason, reason == "completed")
Expand Down Expand Up @@ -1770,6 +1796,11 @@ def _is_no_progress_response(self, response: dict[str, Any]) -> bool:

return state_runtime.is_no_progress_response(response)

def _response_commits_to_code_edit(self, text: str) -> bool:
from villani_code import state_runtime

return state_runtime.response_commits_to_code_edit(text)

def _save_session_snapshot(self, messages: list[dict[str, Any]]) -> None:
from villani_code import state_runtime

Expand Down
22 changes: 22 additions & 0 deletions villani_code/state_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,28 @@ def is_no_progress_response(response: dict[str, Any]) -> bool:
return len(text) <= 2


def response_commits_to_code_edit(text: str) -> bool:
normalized = " ".join(str(text or "").lower().split())
if not normalized:
return False
if any(
phrase in normalized
for phrase in (
"one possible fix would be",
"you could update",
"i recommend changing",
"no code changes are needed",
)
):
return False
edit_verbs = r"(update|modify|change|patch|edit|fix|add)"
if re.search(rf"\bi(?:'ll| will| need to)\s+{edit_verbs}\b", normalized):
return True
if re.search(rf"\bthe fix is to\s+{edit_verbs}\b", normalized):
return True
return False


def save_session_snapshot(runner: Any, messages: list[dict[str, Any]]) -> None:
if getattr(runner, "_mission_state", None) is not None and getattr(runner, "_mission_dir", None) is not None:
mission_dir = runner._mission_dir
Expand Down
Loading