Skip to content

Design: ReAct loop quality improvements + dual-mode execution (ReAct vs Planner) #19

Description

@zjshen14

Background

Research into Claude Code, OpenHands, Cline, Aider, Goose, and Cursor reveals a consistent set of techniques the best agents use to improve loop quality. This issue proposes adopting the most impactful ones and adds a broader architectural question: should OpenCLI support both a ReAct loop and a Planner mode, and when should each be used?


Part 1: ReAct Loop Quality Improvements

1. Tool output truncation (middle-truncation)

Current state: Tool results pass through to context raw with no size limit. A single bash call outputting 10,000 lines consumes most of the context window.

What others do:

  • Claude Code: 30k char hard cap, middle-truncation (head + tail preserved, middle elided), configurable via BASH_MAX_OUTPUT_LENGTH
  • OpenHands: 30k chars, saves full output to a temp file, gives agent the path to read if needed
  • Cursor: 20k chars (tail-only — users report this as a frustration)

Proposal:

  • Default cap: 20k characters per tool result
  • Strategy: middle-truncation — preserve the first ~30% and last ~70% of output; the head contains command context, the tail contains the most recent errors
  • For bash: append [... N lines truncated — full output saved to {SESSION_TMP}/tool-output-<id>.txt ...] so agent can read it if needed
  • Configurable via OPENCLI_MAX_TOOL_OUTPUT env var

2. Max iterations guard

Current state: while (true) in core.ts — no ceiling. Issue #4 covers failing tools but not runaway successful loops.

What others do:

  • Claude Code: maxTurns param (no default, but docs recommend 30 for production)
  • OpenHands: 500 iterations default, GLOBAL_MAX_ITERATIONS for nested agents
  • Goose: --max-turns 1000, separate --max-tool-repetitions for consecutive identical calls

Proposal:

  • Add maxTurns?: number to Agent constructor (default: 50)
  • On breach: yield a final { type: "error", message: "Maximum iterations (N) reached. The task may be too complex — try breaking it into smaller steps." } event and return
  • Add --max-turns CLI flag to opencli chat / opencli run
  • Separately: detect stuck loops (same tool + same args called 3× consecutively) and break early with a targeted message

3. Git context injected at session start

Current state: System prompt injects {CWD} but no git state. Agent wastes its first turn running git status / git log on almost every coding task.

What others do:

  • Claude Code: Injects branch name, default branch, git user, git status --short (capped at 2k chars), last 5 commits from git log --oneline — all at session start, labeled "snapshot in time"
  • Aider: Repo map with ranked symbol graph (heavier but more powerful)
  • Cline/OpenHands/Goose: No static injection — agent runs git commands manually

Proposal:
Add a getGitContext() helper in src/agent/prompt.ts that runs at session start (non-fatal if not a git repo) and injects into the system prompt:

## Repository
Branch: main (default: main)
Status: (clean) / M src/agent/core.ts, ?? dist/
Recent commits:
  abc1234 fix: session tool call IDs
  def5678 feat: Anthropic client

Cap at ~2k chars. Skip gracefully if git is not available or CWD is not a repo.


4. think tool for private reasoning

Current state: No scratchpad. Model must embed reasoning in its visible response text or just act immediately.

What others do:

  • OpenHands: Built-in ThinkTool — always present, cannot be disabled, displayed as collapsible in UI. Anthropic's own research shows the think tool pattern measurably reduces wrong-turn tool calls on complex tasks.
  • Cline: <thinking> XML tags enforced in system prompt (inline, not a function call)
  • Claude Code: System-reminder tunes thinking frequency; effort param enables extended thinking

Proposal:
Register a built-in think tool in createDefaultRegistry():

// Tool definition
name: "think"
description: "Use this to reason privately before acting. The output is never shown to the user — it's a scratchpad for working through complex problems before committing to a tool call."
parameters: { thought: { type: "string" } }
// Execution: no-op, returns { success: true, output: "" }

Display: render as a dim, collapsible block in the renderer (not highlighted like real tool calls). This keeps the visible output clean while giving the model structured reasoning space.


5. System-reminder injection mid-loop

Current state: System instruction is injected once at conversation start. On long sessions, models drift from the original instruction (especially around formatting, git rules, and tool usage).

What others do:

  • Claude Code: Injects tiny system-reminder blocks into tool results mid-loop (e.g., suggesting TodoWrite when tracking is relevant, reminding about git rules when a commit is detected)

Proposal:
After every N tool result rounds (e.g., every 5), append a compact reminder to the tool results message:

[reminder: commit only when explicitly asked; prefer targeted edits over full rewrites; run tests after changes]

Keep it under 50 tokens. This is cheap and highly effective at preventing drift in long sessions.


6. Context compaction (auto-summarize on pressure)

Current state: ContextManager prunes with slice(-maxHistoryMessages) — hard-drops old messages. Issue #2 covers the tool-pair split problem. No summarization.

What others do:

  • Claude Code: Five-layer budget reduction → auto-compact pipeline; older turns are summarized by the model, not dropped
  • Cline: At 80% context utilization, "Auto Compact" summarizes with middle-removal

Proposal (longer term):
Before dropping messages, call the LLM with a compact prompt: "Summarize the key decisions, files changed, and current state from this conversation in under 200 words." Replace the dropped messages with a single synthetic user message containing the summary. Implement behind a --compaction flag initially so it can be tested.


Part 2: ReAct vs Planner Mode

The difference

ReAct Loop Planner Mode
How it works Model reasons and acts interleaved — each tool result informs the next decision Model generates a full step-by-step plan first, user approves, then executes
Best for Simple tasks, exploratory work, quick fixes Complex multi-file refactors, risky operations, unfamiliar codebases
Latency Lower (starts immediately) Higher (plan generation round-trip before any action)
Transparency Low (decisions happen turn-by-turn) High (full plan visible before execution)
User control Low (hard to interrupt mid-chain) High (approve/edit/reject plan before any code changes)
Risk of wrong direction Higher (can go 10 turns down the wrong path) Lower (course-correct at the plan stage)

What others do

  • Cline: Explicit Plan / Act mode — Plan mode is read-only (no writes), Act mode enables all tools. User switches manually or Cline asks.
  • OpenHands: Planning Mode (v1.6.0 beta) — agent generates a plan as a markdown checklist, user approves before any code is written.
  • Aider: Architect mode — one model proposes the plan in plain text, a separate (cheaper) editor model implements it.
  • Claude Code: No explicit planner mode, but the system prompt's three-phase workflow (Research → Plan → Execute) nudges the model to plan internally before acting.

Proposal: support both modes in OpenCLI

Mode A — ReAct (current, default): Unchanged. Best for quick tasks.

Mode B — Planner: Invoked via /plan <task> slash command or --plan CLI flag.

Flow:

  1. Agent runs a read-only planning pass (tools restricted to read, glob, grep, bash with a read-only flag) — no writes, no edits
  2. Produces a numbered markdown plan of steps with file paths and rationale
  3. Displays plan to user; prompts: [A]pprove / [E]dit / [C]ancel
  4. On approval, switches to full ReAct execution using the plan as additional context
  5. Agent checks off steps as it completes them (using the think tool to track progress)

Implementation sketch:

  • Add mode: "react" | "plan" to Agent.run()
  • In plan mode, wrap tool execution with a read-only guard (deny write, edit, bash that writes)
  • Add /plan to the slash command registry in repl.ts
  • The plan output is injected as a skill-like synthetic context block before the execution phase

When to auto-suggest planner mode:
Consider heuristics to automatically suggest /plan when:

  • The user's request contains words like "refactor", "migrate", "redesign", "replace all"
  • The request is longer than ~100 words
  • The request mentions multiple files/modules

This can be a simple keyword match in repl.ts that prints a suggestion: 💡 This looks like a complex task — run /plan first to review a step-by-step approach before making changes.


Summary of proposed changes

# Change Complexity Impact
1 Tool output middle-truncation (20k chars, save to tmp) Small High
2 Max iterations guard (default 50) + stuck detection Small High
3 Git context snapshot at session start Small High
4 Built-in think tool Small Medium
5 Mid-loop system-reminder injection Small Medium
6 Planner mode (/plan command) Medium High
7 Context compaction via LLM summarization Large Medium

Suggested implementation order: 3 → 1 → 2 → 4 → 5 → 6 → 7

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions