Claw Web commited on
Commit
ff5327e
·
1 Parent(s): 38797d2

fix: multi-step agent execution — don't break on text-only when plan has pending steps

Browse files

ROOT CAUSE: Agent loop breaks on ANY text-only response (no tool_calls).
Qwen3 pattern: TodoWrite → text 'now starting' → BREAK (never executes plan)

FIX 1: Smart continuation in agent.ts
- Check todoLists and planMode for pending items before breaking
- If pending work exists, inject continuation prompt and loop again
- Allow up to 2 text-only responses before force-breaking
- Only break immediately if there's no pending work

FIX 2: MULTI-STEP EXECUTION RULES in system prompt
- Explicit instructions: 'You are an AGENT, not a chatbot'
- 'After TodoWrite, MUST immediately start executing with tools'
- 'Text-only responses with pending tasks are a BUG'
- Correct pattern: TodoWrite → bash → bash → SendUserMessage
- Wrong pattern: TodoWrite → text → STOPS

FIX 3: Import todoLists in agent.ts for pending task detection

server/runtime/agent.ts CHANGED
@@ -5,7 +5,7 @@
5
 
6
  import { ENV } from "../_core/env";
7
  import { buildSystemPrompt, TOOL_DEFINITIONS } from "./system-prompt";
8
- import { executeTool, getPlanMode, runPreToolHooks, runPostToolHooks, initializeMcpFromConfig, getMcpManager } from "../tools/executor";
9
  import { compactSession, compactSessionWithLLM, shouldCompact, estimateSessionTokens, dbMessagesToSession, DEFAULT_COMPACTION_CONFIG } from "./compact";
10
  import type { Session, ConversationMessage as CompactMessage, CompactionConfig } from "./compact";
11
  import { UsageTracker, pricingForModel, defaultSonnetTierPricing, estimateCostUsdWithPricing, totalCostUsd, formatUsd, summaryLinesForModel } from "./usage";
@@ -745,14 +745,35 @@ export async function runAgentLoop(
745
  conversationMessages.push(assistantMessage);
746
  assistantMessages.push(assistantMessage);
747
 
748
- // If no tool calls, we're done — the LLM has finished responding
749
  if (!result.toolCalls || result.toolCalls.length === 0) {
750
- // ─── Text loop detection ───
751
- // If model generates text-only responses multiple times in a row without
752
- // tool calls, it's stuck in a self-talk loop. Break after 2 consecutive
753
- // text-only responses (normal flow: 1 text response = done).
754
  consecutiveTextOnly++;
755
- if (consecutiveTextOnly >= 2) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
756
  console.warn(`[agent] Text-only loop detected: ${consecutiveTextOnly} consecutive text responses without tool calls — breaking`);
757
  sendSSE(res, "error", {
758
  message: `⚠️ модель зациклилась в текстовых ответах. попробуй переформулировать запрос`,
 
5
 
6
  import { ENV } from "../_core/env";
7
  import { buildSystemPrompt, TOOL_DEFINITIONS } from "./system-prompt";
8
+ import { executeTool, getPlanMode, runPreToolHooks, runPostToolHooks, initializeMcpFromConfig, getMcpManager, todoLists } from "../tools/executor";
9
  import { compactSession, compactSessionWithLLM, shouldCompact, estimateSessionTokens, dbMessagesToSession, DEFAULT_COMPACTION_CONFIG } from "./compact";
10
  import type { Session, ConversationMessage as CompactMessage, CompactionConfig } from "./compact";
11
  import { UsageTracker, pricingForModel, defaultSonnetTierPricing, estimateCostUsdWithPricing, totalCostUsd, formatUsd, summaryLinesForModel } from "./usage";
 
745
  conversationMessages.push(assistantMessage);
746
  assistantMessages.push(assistantMessage);
747
 
748
+ // If no tool calls, check if the agent should continue or stop
749
  if (!result.toolCalls || result.toolCalls.length === 0) {
 
 
 
 
750
  consecutiveTextOnly++;
751
+
752
+ // ─── Multi-step continuation logic ───────────────────────────
753
+ // Don't break on the first text-only response if there's work to do.
754
+ // The model often generates an intermediate text like "plan created,
755
+ // now starting execution" between planning and doing.
756
+ const currentTodos = todoLists.get(sessionId) || [];
757
+ const hasPendingTodos = currentTodos.some(t => t.status === "pending" || t.status === "in_progress");
758
+ const currentPlan = getPlanMode(sessionId);
759
+ const hasPendingPlan = currentPlan.active && currentPlan.steps.some(
760
+ (s: any) => s.status === "pending" || s.status === "in_progress"
761
+ );
762
+ const hasWorkToDo = hasPendingTodos || hasPendingPlan;
763
+
764
+ if (hasWorkToDo && consecutiveTextOnly <= 2) {
765
+ // Model said something but there are pending tasks — inject a nudge
766
+ // to continue executing, then loop again
767
+ console.info(`[agent] Text-only response but ${hasPendingTodos ? 'pending todos' : 'active plan'} exist — injecting continuation prompt (attempt ${consecutiveTextOnly})`);
768
+ conversationMessages.push({
769
+ role: "user",
770
+ content: "[SYSTEM: You have pending tasks in your plan. Continue executing the next step now using the appropriate tool. Do NOT just describe what you will do — actually DO it by calling tools.]" as string,
771
+ });
772
+ continue; // Don't break — go back to the top of the loop
773
+ }
774
+
775
+ // No pending work, or too many text-only responses — stop
776
+ if (consecutiveTextOnly >= 3) {
777
  console.warn(`[agent] Text-only loop detected: ${consecutiveTextOnly} consecutive text responses without tool calls — breaking`);
778
  sendSSE(res, "error", {
779
  message: `⚠️ модель зациклилась в текстовых ответах. попробуй переформулировать запрос`,
server/runtime/system-prompt.ts CHANGED
@@ -304,7 +304,16 @@ CRITICAL ERROR HANDLING RULES:
304
  - If the SAME tool fails 2 times in a row (even with different arguments), STOP retrying that tool. Explain the error to the user and ask what they want to do.
305
  - NEVER enter a loop of: tool_call → error → slightly different tool_call → same error. If you see this pattern, STOP immediately.
306
  - When you encounter an error, your FIRST response should be to explain the error to the user in plain language, NOT to silently retry.
307
- - Retrying is only appropriate for transient errors like timeouts or network issues, NOT for logical errors like wrong paths or missing files.`
 
 
 
 
 
 
 
 
 
308
  );
309
 
310
 
 
304
  - If the SAME tool fails 2 times in a row (even with different arguments), STOP retrying that tool. Explain the error to the user and ask what they want to do.
305
  - NEVER enter a loop of: tool_call → error → slightly different tool_call → same error. If you see this pattern, STOP immediately.
306
  - When you encounter an error, your FIRST response should be to explain the error to the user in plain language, NOT to silently retry.
307
+ - Retrying is only appropriate for transient errors like timeouts or network issues, NOT for logical errors like wrong paths or missing files.
308
+
309
+ MULTI-STEP EXECUTION RULES:
310
+ - You are an AGENT, not a chatbot. After creating a plan with TodoWrite, you MUST immediately start executing the plan steps by calling tools.
311
+ - NEVER stop after creating a plan. The plan is not the deliverable — the EXECUTED result is.
312
+ - After each tool call result, proceed to the next step. Do NOT generate text-only responses between steps unless you need to explain something to the user.
313
+ - If you have pending tasks in your TodoWrite list, ALWAYS call a tool in your next response. Text-only responses with pending tasks are a BUG.
314
+ - Pattern: TodoWrite (create plan) → bash/read_file/write_file (execute step 1) → bash (execute step 2) → ... → SendUserMessage (report results)
315
+ - WRONG pattern: TodoWrite → "now I will start" (text only, no tool) → STOPS. This is broken. Always include a tool call.
316
+ - When you finish a step, update the todo status to "completed" and immediately proceed to the next pending step.`
317
  );
318
 
319