Why
The ReAct loop runs reflection on a fixed cadence:
# repi/investigation/react_loop.py:408-414, defaults at __init__ ~747-748
enable_reflection=True, reflection_interval=3, max_reflections=2
A reflection is a full LLM round-trip that produces no tool output — it is pure latency (3-10s each) and token cost. The model then needs a further turn to actually act on the reflection. With the defaults that is up to 2 free round-trips per investigation.
For straightforward RCAs — a clear entity ID, or an obvious time window — reflection adds nothing; the loop was already converging. Reflection only earns its cost when the loop is stalling (repeated tool calls returning no new evidence).
Note: the loop already tracks the signal we'd want to gate on — state.consecutive_empty_tool_calls (used today for stall-based early exit at react_loop.py:590).
Options
- Lower the default
max_reflections to 1.
- Make reflection adaptive: only enter the reflection phase when the loop shows stall signals (e.g.
consecutive_empty_tool_calls >= 1 or a run of low-yield tool calls), instead of every reflection_interval actions.
(2) is the more principled fix — reflection fires exactly when it can help and is free otherwise.
Scope (in)
- Gate the
Phase.REFLECTING transition in handle_gathering on a stall signal in addition to / instead of the fixed interval.
- Keep
enable_reflection and the cap as escape hatches.
Scope (out)
- Redesigning the reflection prompt itself.
Acceptance
- On clear-cut eval scenarios, zero reflections fire and answer quality is unchanged.
- On stalling scenarios, reflection still fires.
- Token usage / wall-clock drops measurably on the straightforward eval scenarios.
Relationship to #93
#93 refactors the loop into an explicit FSM where reflection is a phase. This change alters the transition condition into that phase, so it should land on top of / be coordinated with the FSM work to avoid rewriting the same transition twice.
Files
repi/investigation/react_loop.py — handle_gathering (reflection gate ~408-416), defaults ~747-748.
Why
The ReAct loop runs reflection on a fixed cadence:
A reflection is a full LLM round-trip that produces no tool output — it is pure latency (3-10s each) and token cost. The model then needs a further turn to actually act on the reflection. With the defaults that is up to 2 free round-trips per investigation.
For straightforward RCAs — a clear entity ID, or an obvious time window — reflection adds nothing; the loop was already converging. Reflection only earns its cost when the loop is stalling (repeated tool calls returning no new evidence).
Note: the loop already tracks the signal we'd want to gate on —
state.consecutive_empty_tool_calls(used today for stall-based early exit atreact_loop.py:590).Options
max_reflectionsto1.consecutive_empty_tool_calls >= 1or a run of low-yield tool calls), instead of everyreflection_intervalactions.(2) is the more principled fix — reflection fires exactly when it can help and is free otherwise.
Scope (in)
Phase.REFLECTINGtransition inhandle_gatheringon a stall signal in addition to / instead of the fixed interval.enable_reflectionand the cap as escape hatches.Scope (out)
Acceptance
Relationship to #93
#93 refactors the loop into an explicit FSM where reflection is a phase. This change alters the transition condition into that phase, so it should land on top of / be coordinated with the FSM work to avoid rewriting the same transition twice.
Files
repi/investigation/react_loop.py—handle_gathering(reflection gate ~408-416), defaults ~747-748.