Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ DEFAULT_MODEL=bedrock/converse/nvidia.nemotron-nano-3-30b
# =============================================================================
# Minimum Confidence
# =============================================================================
# REVIEW_MIN_CONFIDENCE=0.81
# REVIEW_MIN_CONFIDENCE=0.5


# =============================================================================
Expand Down
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## [Unreleased]

## [2.0.1] - 2026-10-05

### Fixed

- **code_review, supply_chain and scope refinement never found issues.** Every tool call (`read_file`, `search_literal`, `find_*`, `recall_memory`) failed inside the RLM sandbox with `This event loop is already running`. RLM agents now run off the event loop using `asyncio.to_thread()`, and tool calls are bridged back to the event loop via `asyncio.run_coroutine_threadsafe()`. New module `agents/rlm_tools.py` provides `build_rlm_agent()`, `run_rlm()`, and `log_rlm_outcome()`.
- **Reviewers silently discarded issues without an explicit confidence**, because the `Issue.confidence` default (0.8) was below the `review.min_confidence` default (0.81). The code_review, doc and supply_chain prompts and the `Issue.confidence` field description now request a confidence ("≥0.9 verified with tools; 0.7–0.9 strong evidence; <0.7 weak"). The new `filter_by_confidence()` helper (`agents/review/helpers.py`) is used by all three reviewers and logs raw and kept counts per scope.
- **Opus 5.5 on Bedrock refused every RLM call with `content_filter`, so code_review returned nothing (`Empty LM response`).** The trigger was `dspy.RLM`'s stock `reasoning` field description ("Think step-by-step: what do you know? What remains? Plan your next action."). New `CodespyRLM` (`agents/context_safe.py`) replaces it with "Short plan for the next step." and is used by `build_rlm_agent()` and the `ContextSafe` RLM fallback.
- **RLM REPL steps all failed with an empty `Invalid Python syntax`.** dspy's `PythonInterpreter` injects inputs as `name = repr(value)`, and an Enum repr (`<IssueCategory.BUG: 'bug'>`) is not valid Python, so every execution of code_review (`categories`) and supply_chain (`category`) failed before the model's code ran. `CodespyRLM` now passes Enum inputs as their plain values.
- **RLM tool calls with arguments failed in the sandbox** (`get_tree() got an unexpected keyword argument 'max_depth'`, `Tool 'read_file' rejected arguments: ['path']`). dspy.RLM copies each tool's Python signature into the sandbox, and the async-to-sync bridge exposed `sync_wrapper(**kwargs)`, so the sandbox wrapper accepted only a literal `kwargs` argument. `bridge_tools_to_loop()` now sets the bridged function's signature from the tool's args.

### Added

- **Empty LM response diagnostics.** `DiagnosticTwoStepAdapter` (`agents/diagnostic_adapter.py`) is now the default adapter in `configure_dspy()` and `lm_context()`, replacing `TwoStepAdapter`. When a response is empty or null, it logs the model, `max_tokens`, `reasoning_effort`, usage, `finish_reason`, reasoning/thinking blocks, tool calls and request size. A `content_filter` refusal raises `AdapterParseError("model refused (content_filter)")` and writes a dump to `<review.cache_dir>/refusals/<timestamp>-<signature>.json`. Dumps contain the full prompt, including reviewed code. Only keys ending in `_key`, `_secret` or `_token` are redacted.
- **Startup config log.** The review pipeline logs the loaded config file path (new `config.get_loaded_config_path()`), `min_confidence`, and `max_iters`/`max_llm_calls` for code_review, doc and supply_chain.
- **RLM outcome logging.** `log_rlm_outcome()` warns when an RLM run ends in the "Extract forced final output" fallback after tool errors, with the error count and the first error. RLM `verbose` is enabled when the log level is DEBUG (`build_rlm_agent()` and the `ContextSafe` fallback).

### Changed

- **Default `review.min_confidence` (`REVIEW_MIN_CONFIDENCE`) is now 0.5 (was 0.81).** This allows issues to be reported even when the LLM does not explicitly set a confidence score (the `Issue.confidence` default is 0.8, which now passes the threshold). `codespy.yaml`, `.env.example` and the action `min-confidence` description are updated. Override it in codespy.yaml or with the environment variable if needed.

## [2.0.0] - 2026-10-03

### Changed (Breaking)
Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ inputs:
required: false

min-confidence:
description: 'Minimum confidence threshold (0.0-1.0) for reported issues. Default from codespy.yaml: 0.81'
description: 'Minimum confidence threshold (0.0-1.0) for reported issues. Default from codespy.yaml: 0.5'
required: false

# ==========================================
Expand Down
2 changes: 1 addition & 1 deletion codespy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ memory:
# REVIEW
# ============================================================================
review:
min_confidence: 0.81 # REVIEW_MIN_CONFIDENCE
min_confidence: 0.5 # REVIEW_MIN_CONFIDENCE
output_format: markdown # REVIEW_OUTPUT_FORMAT
output_stdout: true # REVIEW_OUTPUT_STDOUT
output_git: true # REVIEW_OUTPUT_GIT
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "codespy-ai"
version = "2.0.0"
version = "2.0.1"
description = "Code review agent powered by DSPy"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/codespy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""codespy - Code review agent powered by DSPy."""

__version__ = "2.0.0"
__version__ = "2.0.1"
45 changes: 44 additions & 1 deletion src/codespy/agents/context_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
exceed the model's context window. Used to wrap all DSPy signature modules.
"""

import enum
import logging
import re

Expand All @@ -19,6 +20,47 @@
# Below this, context rot is not a concern regardless of ratio.
_MIN_RLM_THRESHOLD_TOKENS = 8192

# Replaces dspy.RLM's stock `reasoning` desc ("Think step-by-step: what do you
# know? What remains? Plan your next action."), which on its own triggers Bedrock
# content_filter on Claude Opus 5.5.
RLM_REASONING_DESC = "Short plan for the next step."


def _plain(value): # type: ignore[no-untyped-def]
"""Replace Enum members with their values, recursively.

dspy's PythonInterpreter injects inputs as `name = repr(value)`; an Enum repr
(`<IssueCategory.BUG: 'bug'>`) is not valid Python, so every REPL execution
fails with an empty SyntaxError.
"""
if isinstance(value, enum.Enum):
return value.value
if isinstance(value, (list, tuple, set)):
return [_plain(v) for v in value]
if isinstance(value, dict):
return {_plain(k): _plain(v) for k, v in value.items()}
return value


class CodespyRLM(dspy.RLM):
"""dspy.RLM adjusted for codespy.

- Neutral action-signature `reasoning` description (Bedrock content_filter).
- Enum inputs passed as plain values (REPL variable injection).
"""

def forward(self, **kwargs): # type: ignore[no-untyped-def]
return super().forward(**{k: _plain(v) for k, v in kwargs.items()})

async def aforward(self, **kwargs): # type: ignore[no-untyped-def]
return await super().aforward(**{k: _plain(v) for k, v in kwargs.items()})

def _build_signatures(self): # type: ignore[no-untyped-def]
action_sig, extract_sig = super()._build_signatures()
if "reasoning" in action_sig.output_fields:
action_sig = action_sig.with_updated_fields("reasoning", desc=RLM_REASONING_DESC)
return action_sig, extract_sig


def estimate_context_overflow(model: str, max_tokens: int, input_text: str) -> bool:
"""Estimate whether input + max_tokens would exceed the model's context window.
Expand Down Expand Up @@ -197,9 +239,10 @@ def _get_current_signature(self):

def _create_rlm_fallback(self) -> dspy.RLM:
"""Create RLM with current signature and tools."""
return dspy.RLM(
return CodespyRLM(
self._get_current_signature(),
tools=self._tools,
max_iters=self._max_iters,
max_llm_calls=self._max_llm_calls,
verbose=logger.isEnabledFor(logging.DEBUG),
)
Loading
Loading