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
21 changes: 21 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,27 @@ DEFAULT_MODEL=anthropic/claude-opus-4-6
# Minimum confidence threshold for reported issues (0.0-1.0)
# MIN_CONFIDENCE=0.81

# =============================================================================
# RLM Fallback (Context Rot Prevention)
# =============================================================================
# Proactive fallback to RLM when input tokens exceed a ratio of the model's
# max_input_tokens. Prevents quality degradation ("context rot") that occurs
# well before the hard context window limit. The hard overflow safety net
# remains active regardless of these settings.
#
# Thresholds are per module type:
# react: Multi-step tool-using agents (code_review, scope, supply_chain)
# chain_of_thought: Single-pass reasoning (doc, summary, audit)
# predict: Basic completion (not currently used)
#
# Minimum floor: proactive threshold never fires below 8192 input tokens.

# RLM_FALLBACK_ENABLED=true
# RLM_FALLBACK_REACT_THRESHOLD=0.30
# RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD=0.40
# RLM_FALLBACK_PREDICT_THRESHOLD=0.50


# =============================================================================
# Memory (Hippocampus)
# =============================================================================
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Changelog

## [1.0.5] - 2026-08-18

### Added
- Proactive RLM fallback with configurable context rot thresholds per module type (ReAct: 0.30, ChainOfThought: 0.40, Predict: 0.50)
- New `rlm_fallback` config section with `enabled`, `react_threshold`, `chain_of_thought_threshold`, `predict_threshold`
- GitHub Action inputs: `rlm-fallback-enabled`, `rlm-fallback-react-threshold`, `rlm-fallback-chain-of-thought-threshold`, `rlm-fallback-predict-threshold`

### Changed
- `ContextSafe` now checks proactive context rot threshold before checking hard overflow (existing overflow detection retained as safety net)
- `ContextSafe._would_overflow` renamed to `_should_use_rlm` with expanded three-layer logic

## [1.0.4] - 2026-08-18

### Fixed
Expand Down
33 changes: 33 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,27 @@ inputs:
required: false
default: '30'

# RLM fallback (context rot prevention)
rlm-fallback-enabled:
description: 'Enable proactive RLM fallback before context window overflow'
required: false
default: 'true'

rlm-fallback-react-threshold:
description: 'Context ratio for ReAct modules (0.0-1.0). RLM used when input exceeds this ratio of max_input_tokens.'
required: false
default: '0.30'

rlm-fallback-chain-of-thought-threshold:
description: 'Context ratio for ChainOfThought modules (0.0-1.0). RLM used when input exceeds this ratio.'
required: false
default: '0.40'

rlm-fallback-predict-threshold:
description: 'Context ratio for Predict modules (0.0-1.0). RLM used when input exceeds this ratio.'
required: false
default: '0.50'

min-confidence:
description: 'Minimum confidence threshold (0.0-1.0) for reported issues'
required: false
Expand Down Expand Up @@ -390,6 +411,12 @@ runs:
DEFAULT_MAX_LLM_CALLS: ${{ inputs.default-max-llm-calls }}
MIN_CONFIDENCE: ${{ inputs.min-confidence }}

# RLM fallback
RLM_FALLBACK_ENABLED: ${{ inputs.rlm-fallback-enabled }}
RLM_FALLBACK_REACT_THRESHOLD: ${{ inputs.rlm-fallback-react-threshold }}
RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD: ${{ inputs.rlm-fallback-chain-of-thought-threshold }}
RLM_FALLBACK_PREDICT_THRESHOLD: ${{ inputs.rlm-fallback-predict-threshold }}

# Scope identification signature
SCOPE_ENABLED: ${{ inputs.scope-enabled }}
SCOPE_MODEL: ${{ inputs.scope-model }}
Expand Down Expand Up @@ -476,6 +503,12 @@ runs:
[ -n "$DEFAULT_MAX_LLM_CALLS" ] && DOCKER_ARGS="$DOCKER_ARGS -e DEFAULT_MAX_LLM_CALLS"
[ -n "$MIN_CONFIDENCE" ] && DOCKER_ARGS="$DOCKER_ARGS -e MIN_CONFIDENCE"

# RLM fallback
[ -n "$RLM_FALLBACK_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_ENABLED"
[ -n "$RLM_FALLBACK_REACT_THRESHOLD" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_REACT_THRESHOLD"
[ -n "$RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD"
[ -n "$RLM_FALLBACK_PREDICT_THRESHOLD" ] && DOCKER_ARGS="$DOCKER_ARGS -e RLM_FALLBACK_PREDICT_THRESHOLD"

# Scope identification
[ -n "$SCOPE_ENABLED" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_ENABLED"
[ -n "$SCOPE_MODEL" ] && DOCKER_ARGS="$DOCKER_ARGS -e SCOPE_MODEL"
Expand Down
22 changes: 22 additions & 0 deletions codespy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,28 @@ memory:
max_tokens: null # MEMORY_CARTOGRAPHER_MAX_TOKENS


# ============================================================================
# RLM FALLBACK (CONTEXT ROT PREVENTION)
# ============================================================================
# Proactive fallback to RLM when input tokens exceed a ratio of the model's
# max_input_tokens. Prevents quality degradation that occurs well before the
# hard context window limit. The hard overflow safety net remains active
# regardless of these settings.
#
# Thresholds are per DSPy module type:
# - react: Multi-step tool-using agents (code_review, scope, supply_chain)
# - chain_of_thought: Single-pass reasoning (doc, summary, audit)
# - predict: Basic completion (not currently used)
#
# Minimum floor: proactive threshold never fires below 8192 input tokens.

rlm_fallback:
enabled: true # RLM_FALLBACK_ENABLED
react_threshold: 0.30 # RLM_FALLBACK_REACT_THRESHOLD
chain_of_thought_threshold: 0.40 # RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD
predict_threshold: 0.50 # RLM_FALLBACK_PREDICT_THRESHOLD


# ============================================================================
# SIGNATURES
# ============================================================================
Expand Down
23 changes: 14 additions & 9 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,26 @@ See [Configuration](configuration.md) for per-signature settings.

### Context Window Overflow Resilience

All signature modules are wrapped in `ContextSafe`, which provides transparent
fallback to `dspy.RLM` (Recursive Language Model) when the input exceeds the
model's context window.

- **Pre-flight**: For models in litellm's database, estimates input tokens
before calling the module. If overflow is predicted, skips directly to RLM.
- **Try/catch**: For models not in litellm's database, catches the provider
error and retries with RLM automatically.
All signature modules are wrapped in `ContextSafe`, which provides three-layer
defense against context-related quality degradation:

1. **Proactive threshold (context rot)**: Estimates input tokens and compares
against a configurable ratio of `max_input_tokens`. When the threshold is
exceeded (and input >= 8192 tokens), skips the inner module and uses RLM
directly — preventing quality degradation that occurs well before the hard
context limit. Thresholds differ by module type: ReAct (0.30),
ChainOfThought (0.40), Predict (0.50).
2. **Pre-flight overflow**: For models in litellm's database, detects when
input + output budget + safety margin would exceed the hard context limit.
3. **Try/catch**: For models not in litellm's database, catches the provider
error and retries with RLM automatically.

RLM puts inputs into a sandboxed Python interpreter rather than the LLM prompt.
The model writes code to access and process variables (e.g., chunking large
patches via `llm_query()`), eliminating context window limits entirely.

The composition order is: `Hippocampus(ContextSafe(Module))` — Hippocampus
handles memory, ContextSafe handles overflow, each with single responsibility.
handles memory, ContextSafe handles context quality, each with single responsibility.

## Hippocampus Memory

Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,10 @@ AUTO_DISCOVER_GEMINI=false
| Temperature | `DEFAULT_TEMPERATURE` | `0.2` | Default temperature for LLM calls |
| Max iterations | `DEFAULT_MAX_ITERS` | `10` | Maximum ReAct iterations for tool-using agents |
| Prompt caching | `ENABLE_PROMPT_CACHING` | `true` | Provider-side prompt caching (Anthropic, OpenAI, Bedrock) |
| RLM fallback | `RLM_FALLBACK_ENABLED` | `true` | Proactive RLM fallback for context rot prevention |
| RLM react threshold | `RLM_FALLBACK_REACT_THRESHOLD` | `0.30` | Context ratio triggering RLM for ReAct modules |
| RLM CoT threshold | `RLM_FALLBACK_CHAIN_OF_THOUGHT_THRESHOLD` | `0.40` | Context ratio triggering RLM for ChainOfThought modules |
| RLM predict threshold | `RLM_FALLBACK_PREDICT_THRESHOLD` | `0.50` | Context ratio triggering RLM for Predict modules |

## Recommended Model Strategy

Expand Down
68 changes: 51 additions & 17 deletions src/codespy/agents/context_safe.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
# Regex for detecting context window overflow in error messages
_RE_CONTEXT_LENGTH = re.compile(r"maximum context length is \d+ tokens", re.IGNORECASE)

# Minimum input tokens before proactive threshold applies.
# Below this, context rot is not a concern regardless of ratio.
_MIN_RLM_THRESHOLD_TOKENS = 8192


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 @@ -67,6 +71,7 @@ def __init__(
name: str = "",
max_iters: int | None = None,
max_llm_calls: int | None = None,
rlm_threshold: float = 1.0,
):
super().__init__()
self.module = module
Expand All @@ -75,6 +80,7 @@ def __init__(
self._name = name or signature.__name__
self._max_iters = max_iters
self._max_llm_calls = max_llm_calls
self._rlm_threshold = rlm_threshold

@property
def signature(self):
Expand All @@ -85,12 +91,11 @@ def signature(self, value):
self.module.signature = value

def forward(self, **kwargs) -> dspy.Prediction:
if self._would_overflow(kwargs):
should_fallback, reason = self._should_use_rlm(kwargs)
if should_fallback:
logger.warning(
"ContextSafe[%s]: pre-flight detected context overflow for model=%s; "
"falling back to RLM",
self._name,
getattr(dspy.settings.lm, "model", "unknown"),
"ContextSafe[%s]: %s for model=%s; falling back to RLM",
self._name, reason, getattr(dspy.settings.lm, "model", "unknown"),
)
return self._create_rlm_fallback()(**kwargs)

Expand All @@ -110,12 +115,11 @@ def forward(self, **kwargs) -> dspy.Prediction:

async def aforward(self, **kwargs) -> dspy.Prediction:
"""Async path — used by code_review, scope, supply_chain via Hippocampus.aforward."""
if self._would_overflow(kwargs):
should_fallback, reason = self._should_use_rlm(kwargs)
if should_fallback:
logger.warning(
"ContextSafe[%s]: pre-flight detected context overflow for model=%s; "
"falling back to RLM",
self._name,
getattr(dspy.settings.lm, "model", "unknown"),
"ContextSafe[%s]: %s for model=%s; falling back to RLM",
self._name, reason, getattr(dspy.settings.lm, "model", "unknown"),
)
return await self._create_rlm_fallback().acall(**kwargs)

Expand All @@ -133,20 +137,50 @@ async def aforward(self, **kwargs) -> dspy.Prediction:
)
return await self._create_rlm_fallback().acall(**kwargs)

def _would_overflow(self, kwargs: dict) -> bool:
"""Pre-flight: estimate overflow from current LM config and input size."""
def _should_use_rlm(self, kwargs: dict) -> tuple[bool, str]:
"""Check if RLM should be used (proactive threshold or hard overflow).

Returns (should_fallback, reason) for logging.
"""
try:
lm = dspy.settings.lm
if lm is None:
return False
return False, ""
model = lm.model
max_tokens = lm.kwargs.get("max_tokens") or 0
if not max_tokens:
return False

info = litellm.get_model_info(model)
max_input = info.get("max_input_tokens") or 0
if not max_input:
return False, ""

input_text = "\n".join(str(v) for v in kwargs.values())
return estimate_context_overflow(model, max_tokens, input_text)
estimated_input = litellm.token_counter(model=model, text=input_text)

# Layer 1: Proactive threshold (context rot prevention)
# Only applies when input exceeds the minimum floor (8192 tokens) —
# below that, context rot is not a concern.
if (self._rlm_threshold < 1.0
and estimated_input >= _MIN_RLM_THRESHOLD_TOKENS):
threshold_tokens = int(max_input * self._rlm_threshold)
if estimated_input > threshold_tokens:
return True, (
f"context rot threshold exceeded "
f"({estimated_input} > {threshold_tokens} = "
f"{self._rlm_threshold:.0%} of {max_input})"
)

# Layer 2: Hard overflow check (existing safety net)
SAFETY_MARGIN = 4096
if max_tokens and (estimated_input + max_tokens + SAFETY_MARGIN) > max_input:
return True, (
f"context overflow predicted "
f"({estimated_input} + {max_tokens} + {SAFETY_MARGIN} > {max_input})"
)

return False, ""
except Exception:
return False
return False, ""

def _get_current_signature(self):
"""Get current signature (may include context_memory if Hippocampus modified it)."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ class Cartographer(dspy.Module):

def __init__(self):
super().__init__()
self.predict = ContextSafe(dspy.ChainOfThought(CartographerSig), CartographerSig, name="cartographer")
from codespy.config import settings # lazy to avoid circular import
self.predict = ContextSafe(
dspy.ChainOfThought(CartographerSig), CartographerSig, name="cartographer",
rlm_threshold=settings.get_rlm_threshold("chain_of_thought"),
)

def forward(self, diagnosis, item_tags, cache_candidates, current_map, question,
token_budget, current_tokens, max_context_item_tokens):
Expand Down
6 changes: 5 additions & 1 deletion src/codespy/agents/memory/hippocampus/modules/distiller.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ class Distiller(dspy.Module):

def __init__(self):
super().__init__()
self.predict = ContextSafe(dspy.ChainOfThought(DistillerSig), DistillerSig, name="distiller")
from codespy.config import settings # lazy to avoid circular import
self.predict = ContextSafe(
dspy.ChainOfThought(DistillerSig), DistillerSig, name="distiller",
rlm_threshold=settings.get_rlm_threshold("chain_of_thought"),
)

def forward(
self,
Expand Down
1 change: 1 addition & 0 deletions src/codespy/agents/reviewer/modules/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ def forward(
AuditSignature,
name="audit",
max_llm_calls=self._settings.get_max_llm_calls("audit"),
rlm_threshold=self._settings.get_rlm_threshold("chain_of_thought"),
)
logger.info("Running audit...")

Expand Down
1 change: 1 addition & 0 deletions src/codespy/agents/reviewer/modules/code_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ async def aforward(
name="code_review",
max_iters=max_iters,
max_llm_calls=self._settings.get_max_llm_calls("code_review"),
rlm_threshold=self._settings.get_rlm_threshold("react"),
)
scoped = make_scope_relative(scope)
logger.info(
Expand Down
1 change: 1 addition & 0 deletions src/codespy/agents/reviewer/modules/doc_reviewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ async def aforward(
DocReviewSignature,
name="doc",
max_llm_calls=self._settings.get_max_llm_calls("doc"),
rlm_threshold=self._settings.get_rlm_threshold("chain_of_thought"),
)
logger.info(
f" Doc review: scope {scope.subroot} "
Expand Down
1 change: 1 addition & 0 deletions src/codespy/agents/reviewer/modules/scope_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,7 @@ async def _refine_scopes(
name="scope",
max_iters=max_iters,
max_llm_calls=self._settings.get_max_llm_calls("scope"),
rlm_threshold=self._settings.get_rlm_threshold("react"),
)
mem: Hippocampus | None = None

Expand Down
1 change: 1 addition & 0 deletions src/codespy/agents/reviewer/modules/summarizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ def forward(
PRSummarySignature,
name="summary",
max_llm_calls=self._settings.get_max_llm_calls("summary"),
rlm_threshold=self._settings.get_rlm_threshold("chain_of_thought"),
)
logger.info("Generating PR summary...")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ async def aforward(
name="supply_chain",
max_iters=supply_chain_max_iters,
max_llm_calls=self._settings.get_max_llm_calls("supply_chain"),
rlm_threshold=self._settings.get_rlm_threshold("react"),
)

logger.debug(
Expand Down
Loading
Loading