Add Phase 6: write tools, shell, and edit tools with reasoning-only fix

Implement 6 new agent tools — write_file, make_dir, delete_file,
str_replace, patch_apply, run_command — bringing the agent from
read-only observer to active code modifier. All write/shell operations
are gated through the existing permissions service.

Also fix a bug where qwen3.5 thinking mode produces reasoning tokens
but no content after tool results, causing the agent to silently exit.
The loop now detects reasoning-only responses, retries twice, then
injects a nudge message to break the model out of its thinking loop.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 09:45:48 -05:00
parent 0cf0d01657
commit f60c47a85f
12 changed files with 956 additions and 3 deletions

View File

@@ -41,6 +41,13 @@ class SessionContext:
self._message_count += 1
return message
def pop_last_message(self) -> Message | None:
"""Remove and return the last message, or None if history is empty."""
if self._history:
self._message_count -= 1
return self._history.pop()
return None
def get_history(self) -> list[Message]:
"""Return a shallow copy of the conversation history."""
return list(self._history)

View File

@@ -23,6 +23,8 @@ from app.utils.logging import get_logger
logger = get_logger(__name__)
_MAX_REASONING_RETRIES = 2
class AgentLoop:
"""ReAct-style agent loop that streams LLM responses and executes tool calls.
@@ -81,6 +83,7 @@ class AgentLoop:
self._ctx.add_message("user", user_input)
max_iter = self._config.agent.max_iterations
reasoning_only_streak = 0
for iteration in range(1, max_iter + 1):
# Check token budget
if self._ctx.token_counter.is_over_budget():
@@ -102,6 +105,9 @@ class AgentLoop:
tool_calls=assistant_msg.tool_calls,
)
# Detect reasoning-only response (model thought but produced nothing)
reasoning_only = self._handler.had_reasoning_only
# Record token usage
if self._handler.usage:
self._ctx.token_counter.count_usage(self._handler.usage)
@@ -114,6 +120,29 @@ class AgentLoop:
self._handler.reset()
# Reasoning-only: model produced thinking tokens but no content or tool calls.
if reasoning_only:
reasoning_only_streak += 1
self._ctx.pop_last_message()
if reasoning_only_streak >= _MAX_REASONING_RETRIES:
# Nudge the model by injecting a user hint
print_warning(
f"Model produced reasoning but no response {reasoning_only_streak} times. "
"Nudging model to respond..."
)
self._ctx.add_message(
"user",
"Please respond with your answer. Do not just think — provide your actual response.",
)
reasoning_only_streak = 0
else:
print_warning("Model produced reasoning but no response. Retrying...")
continue
# Successful response — reset streak
reasoning_only_streak = 0
# No tool calls → task complete (plain text response)
if not assistant_msg.tool_calls:
break