# Plan: Add a `think` Tool

## Goal

Add a structured thinking tool to the agent, inspired by the `sequentialthinking` MCP server. This gives the model a scratchpad for multi-step reasoning with support for revisions and branching, making complex tasks more reliable.

## Why

The agent currently has no way to "think out loud" in a structured way. When tackling multi-step problems (planning file edits, debugging, weighing alternatives), the model either dumps reasoning into its text response or just wings it. A dedicated tool lets it:

- Break problems into numbered steps it can refer back to
- Revise earlier conclusions when new information arrives
- Branch into alternative approaches and compare them
- Signal when it needs more thinking before acting

The thinking history is tracked in memory and logged to stderr, so the user can see the reasoning process with `--verbose`.

## Design

### Tool Schema

```python
{
    "type": "function",
    "function": {
        "name": "think",
        "description": "Structure your reasoning into numbered steps. Use this before complex tasks to plan, during debugging to track hypotheses, or when you need to revise earlier conclusions. Supports revisions (correcting a previous thought) and branches (exploring alternatives).",
        "parameters": {
            "type": "object",
            "properties": {
                "thought": {
                    "type": "string",
                    "description": "Your current thinking step. Max 10000 characters."
                },
                "thought_number": {
                    "type": "integer",
                    "description": "Current step number (1-based).",
                    "minimum": 1
                },
                "total_thoughts": {
                    "type": "integer",
                    "description": "Estimated total steps needed. Adjust freely as your understanding evolves.",
                    "minimum": 1
                },
                "next_thought_needed": {
                    "type": "boolean",
                    "description": "true if you need more thinking steps, false when done."
                },
                "is_revision": {
                    "type": "boolean",
                    "description": "Set true if this thought corrects or updates a previous one. Must also set revises_thought."
                },
                "revises_thought": {
                    "type": "integer",
                    "description": "Required when is_revision is true. Which thought number is being revised.",
                    "minimum": 1
                },
                "branch_from_thought": {
                    "type": "integer",
                    "description": "Thought number to branch from. Must be set together with branch_id.",
                    "minimum": 1
                },
                "branch_id": {
                    "type": "string",
                    "description": "Required when branch_from_thought is set. Label for the branch (e.g. 'approach-b'). 1-50 characters.",
                    "minLength": 1,
                    "maxLength": 50
                }
            },
            "required": ["thought", "thought_number", "total_thoughts", "next_thought_needed"]
        }
    }
}
```

### State Management

A `ThinkingState` class in a new `thinking.py` module holds:

```python
@dataclass
class ThoughtEntry:
    thought: str
    thought_number: int
    total_thoughts: int
    next_thought_needed: bool
    is_revision: bool = False
    revises_thought: int | None = None
    branch_from_thought: int | None = None
    branch_id: str | None = None

class ThinkingState:
    history: list[ThoughtEntry]
    branches: dict[str, list[ThoughtEntry]]
    verbose: bool  # set at construction from args.verbose
```

- One `ThinkingState` instance lives in `agent.py`, created at startup with `verbose=args.verbose`.
- State resets each agent run (no persistence needed).

### Validation

`ThinkingState.process()` validates before recording. On invalid input, it returns an error string (same convention as other tools). Validations:

| Rule | Error returned |
|------|----------------|
| `is_revision=True` but `revises_thought` missing | `"error: is_revision requires revises_thought"` |
| `revises_thought` set but `is_revision` not `True` | `"error: revises_thought requires is_revision=true"` |
| `revises_thought` references a thought_number not in history | `"error: revises_thought=N but only M thoughts recorded"` |
| `branch_from_thought` set without `branch_id` | `"error: branch_from_thought requires branch_id"` |
| `branch_id` set without `branch_from_thought` | `"error: branch_id requires branch_from_thought"` |
| `branch_from_thought` references a thought_number not in history | `"error: branch_from_thought=N but only M thoughts recorded"` |
| `branch_id` empty or whitespace-only after trim | `"error: branch_id must not be blank"` |
| `branch_id` exceeds 50 chars | `"error: branch_id exceeds 50 character limit"` |
| More than 20 distinct branches | `"error: too many branches (20 max)"` |
| `thought` exceeds 10000 chars | Silently truncated to 10000 chars |
| `history` already has 200 entries | `"error: thinking history full (200 steps max)"` |

### Return Format

`process()` always returns a **string**, not a dict. This matches the `dispatch() -> str` contract that `agent.py` relies on (`content` field is always a string at line 243).

On success, the string is a compact JSON-encoded summary:

```python
json.dumps({
    "thought_number": 2,
    "total_thoughts": 5,
    "next_thought_needed": True,
    "branches": ["approach-b"],
    "history_length": 2
})
```

On validation error, it's a plain error string like `"error: is_revision requires revises_thought"` (lowercase `error:`, matching the convention in `tools.py`).

This means `dispatch()` can return the result of `process()` directly — no special handling, no type mismatch.

### Bounds

Consistent with the existing output caps in `tools.py`:

| Limit | Value | Rationale |
|-------|-------|-----------|
| Max thought text | 10000 chars | Prevent context bloat per call |
| Max history entries | 200 | Cap total memory/context cost |
| Max branch_id length | 50 chars | Caps branch label size |
| Max branches | 20 | Prevents unbounded branch list growth |
| Returned JSON size | Bounded | Metadata only: 5 fixed fields + up to 20 branch IDs (each ≤50 chars). Worst case ~1200 bytes. |

The thought text is stored in `ThinkingState` but never echoed back in the tool result — only the metadata summary is returned. The model already has the thought in its context (it just sent it). This keeps tool results small.

### Logging

`ThinkingState` handles its own stderr logging internally. This is the **only** logging path for think calls.

**In `ThinkingState.process()`:** If `self.verbose` is `True`, write a formatted line to stderr:

```
[think 2/5] Checking if the regex handles edge cases...
[think 3/5 rev:1] Actually, the regex also needs to handle...
[think 3/5 branch:approach-b from:2] What if we use glob instead...
```

Thought text is normalized before logging: newlines replaced with spaces, consecutive whitespace collapsed, then truncated to 200 chars. This keeps each `[think ...]` entry on a single line.

**In `agent.py`:** Skip the generic `log(f"Tool call: ...")` and `log(f"Tool result: ...")` lines when `name == "think"`. This avoids double-logging the thought text. The existing `log()` calls for all other tools remain unchanged.

No color dependencies — just plain text prefixes.

### Integration Points

**tools.py:**
- Add the schema dict to `TOOLS`.
- In `dispatch()`, add a `"think"` branch. Pass `**kwargs` through so it can receive `thinking_state`.
- Signature becomes: `dispatch(name: str, args: dict, base_dir: str, **kwargs) -> str`. The `thinking_state` key is extracted inside the `"think"` branch only. All existing callers pass no extra kwargs and are unaffected.
- If `thinking_state` is missing from kwargs when `name == "think"`, return `"error: think tool is not available"` (don't raise — follows the tool error convention).

**agent.py:**
- Create `ThinkingState(verbose=args.verbose)` before the loop.
- Pass `thinking_state=thinking_state` to `dispatch()`.
- Skip generic tool logging when `name == "think"`.

**thinking.py (new file):**
- `ThoughtEntry` dataclass.
- `ThinkingState` class with `process()` method and `format_log_line()`.
- All validation logic lives here.

### What This Does NOT Do

- No persistent storage. Thoughts live only during one agent run.
- No UI. Just stderr logs and tool results returned to the model.
- No forced usage. The model calls `think` when it decides to; it's just another tool.
- No separate process or server. It's an in-process tool like the others.

## Files to Change

| File | Change |
|------|--------|
| `thinking.py` | New. ~80 lines. `ThoughtEntry`, `ThinkingState`, validation, formatting. |
| `tools.py` | Add `think` schema to `TOOLS`. Add `"think"` branch in `dispatch()`. Change signature to `dispatch(name, args, base_dir, **kwargs)`. |
| `agent.py` | Create `ThinkingState`, pass to `dispatch()`. Skip generic logging for think calls. |
| `tmp/test_think.py` | Tests for `ThinkingState`. |

## Testing

### New tests (`tmp/test_think.py`)

Unit tests for `ThinkingState.process()`:

- **Linear flow**: 3 sequential thoughts, verify history_length increments, returned JSON parses correctly.
- **Revision**: Valid revision (is_revision + revises_thought pointing to existing thought). Verify history records it.
- **Revision validation**: is_revision=True without revises_thought returns error string. revises_thought referencing non-existent thought returns error string. revises_thought without is_revision returns error string.
- **Branching**: Valid branch (both fields set, from existing thought). Verify branch appears in returned `branches` list.
- **Branch validation**: branch_from_thought without branch_id returns error. branch_id without branch_from_thought returns error. branch_from_thought referencing non-existent thought returns error. branch_id > 50 chars returns error. 21st distinct branch returns error.
- **Auto-adjust**: thought_number > total_thoughts causes total_thoughts to increase in response.
- **Truncation**: Thought > 10000 chars is silently truncated, no error.
- **History cap**: 201st thought returns error string.
- **Log formatting**: With verbose=True, capture stderr and verify format is single-line. With verbose=False, verify no stderr output.
- **Newline normalization**: Thought containing `\n` produces a single-line log entry with spaces instead of newlines.

### Regression tests for existing tools

- Verify `dispatch("read_file", ..., base_dir)` still works without `thinking_state` kwarg.
- Verify `dispatch("write_file", ..., base_dir)` still works.
- Verify `dispatch("grep", ..., base_dir)` still works.
- Verify `dispatch("think", ..., base_dir)` without `thinking_state` kwarg returns `"error: think tool is not available"`.
- These can be added to existing test files or as a small section in `tmp/test_think.py`.

### Agent integration test (`tmp/test_think.py`)

- **Log skip**: Monkeypatch `agent.log` (or capture stderr), simulate a tool loop iteration with `name == "think"`, verify the generic `"Tool call: ..."` and `"Tool result: ..."` lines are **not** emitted. Then simulate with `name == "read_file"` and verify they **are** emitted.

### Manual test

Run the agent on a multi-step task with `-v` and verify:
- Thinking steps appear on stderr with `[think N/M]` prefix.
- No duplicate logging from the generic tool log lines.
- Final answer appears on stdout as usual.
