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:
- Agent runs a read-only planning pass (tools restricted to
read, glob, grep, bash with a read-only flag) — no writes, no edits
- Produces a numbered markdown plan of steps with file paths and rationale
- Displays plan to user; prompts:
[A]pprove / [E]dit / [C]ancel
- On approval, switches to full ReAct execution using the plan as additional context
- 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
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
bashcall outputting 10,000 lines consumes most of the context window.What others do:
BASH_MAX_OUTPUT_LENGTHProposal:
[... N lines truncated — full output saved to {SESSION_TMP}/tool-output-<id>.txt ...]so agent can read it if neededOPENCLI_MAX_TOOL_OUTPUTenv var2. Max iterations guard
Current state:
while (true)incore.ts— no ceiling. Issue #4 covers failing tools but not runaway successful loops.What others do:
maxTurnsparam (no default, but docs recommend 30 for production)GLOBAL_MAX_ITERATIONSfor nested agents--max-turns 1000, separate--max-tool-repetitionsfor consecutive identical callsProposal:
maxTurns?: numbertoAgentconstructor (default: 50){ type: "error", message: "Maximum iterations (N) reached. The task may be too complex — try breaking it into smaller steps." }event and return--max-turnsCLI flag toopencli chat/opencli run3. Git context injected at session start
Current state: System prompt injects
{CWD}but no git state. Agent wastes its first turn runninggit status/git logon almost every coding task.What others do:
git status --short(capped at 2k chars), last 5 commits fromgit log --oneline— all at session start, labeled "snapshot in time"Proposal:
Add a
getGitContext()helper insrc/agent/prompt.tsthat runs at session start (non-fatal if not a git repo) and injects into the system prompt:Cap at ~2k chars. Skip gracefully if
gitis not available or CWD is not a repo.4.
thinktool for private reasoningCurrent state: No scratchpad. Model must embed reasoning in its visible response text or just act immediately.
What others do:
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.<thinking>XML tags enforced in system prompt (inline, not a function call)effortparam enables extended thinkingProposal:
Register a built-in
thinktool increateDefaultRegistry():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:
system-reminderblocks into tool results mid-loop (e.g., suggestingTodoWritewhen 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:
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:
ContextManagerprunes withslice(-maxHistoryMessages)— hard-drops old messages. Issue #2 covers the tool-pair split problem. No summarization.What others do:
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 syntheticusermessage containing the summary. Implement behind a--compactionflag initially so it can be tested.Part 2: ReAct vs Planner Mode
The difference
What others do
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--planCLI flag.Flow:
read,glob,grep,bashwith a read-only flag) — no writes, no edits[A]pprove / [E]dit / [C]ancelthinktool to track progress)Implementation sketch:
mode: "react" | "plan"toAgent.run()write,edit,bashthat writes)/planto the slash command registry inrepl.tsWhen to auto-suggest planner mode:
Consider heuristics to automatically suggest
/planwhen:This can be a simple keyword match in
repl.tsthat 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
thinktool/plancommand)Suggested implementation order: 3 → 1 → 2 → 4 → 5 → 6 → 7