File size: 8,865 Bytes
359acf6
31d646f
359acf6
 
31d646f
359acf6
31d646f
359acf6
 
 
31d646f
359acf6
31d646f
359acf6
 
 
 
31d646f
359acf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31d646f
359acf6
 
 
31d646f
359acf6
 
 
 
31d646f
359acf6
 
 
31d646f
359acf6
31d646f
359acf6
 
31d646f
359acf6
31d646f
359acf6
 
 
 
 
 
 
 
31d646f
359acf6
31d646f
359acf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31d646f
 
359acf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31d646f
359acf6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31d646f
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""Plan mode tools β€” EnterPlanMode and ExitPlanMode.

EnterPlanMode switches the agent into a reasoning/planning mode where it
explores the codebase read-only before writing any code.

ExitPlanMode exits planning mode and returns to normal execution mode.

Plan state is stored in ~/.stack-2.9/plan_mode.json
Reasoning steps are tracked in ~/.stack-2.9/plan_reasoning.json
"""

from __future__ import annotations

import json
import os
from datetime import datetime, timezone
from typing import Any

from .base import BaseTool, ToolResult
from .registry import get_registry

DATA_DIR = os.path.expanduser("~/.stack-2.9")
PLAN_STATE_FILE = os.path.join(DATA_DIR, "plan_mode.json")
REASONING_FILE = os.path.join(DATA_DIR, "plan_reasoning.json")


def _load_plan_state() -> dict[str, Any]:
    os.makedirs(DATA_DIR, exist_ok=True)
    if os.path.exists(PLAN_STATE_FILE):
        try:
            with open(PLAN_STATE_FILE) as f:
                return json.load(f)
        except Exception:
            pass
    return {"active": False, "entered_at": None, "plan_text": None, "context": None}


def _save_plan_state(state: dict[str, Any]) -> None:
    os.makedirs(DATA_DIR, exist_ok=True)
    with open(PLAN_STATE_FILE, "w") as f:
        json.dump(state, f, indent=2, default=str)


def _load_reasoning() -> list[dict[str, Any]]:
    os.makedirs(DATA_DIR, exist_ok=True)
    if os.path.exists(REASONING_FILE):
        try:
            with open(REASONING_FILE) as f:
                return json.load(f)
        except Exception:
            pass
    return []


def _save_reasoning(steps: list[dict[str, Any]]) -> None:
    os.makedirs(DATA_DIR, exist_ok=True)
    with open(REASONING_FILE, "w") as f:
        json.dump(steps, f, indent=2, default=str)


# ── EnterPlanModeTool ───────────────────────────────────────────────────────────


class EnterPlanModeTool(BaseTool[dict[str, Any], dict[str, Any]]):
    """Enter plan mode β€” a read-only reasoning phase for exploring and designing.

    Parameters
    ----------
    plan_text : str, optional
        Initial plan text to record.
    context : str, optional
        Context or task description for the plan.
    """

    name = "EnterPlanMode"
    description = (
        "Switch to plan mode for complex tasks requiring exploration and design. "
        "In plan mode, you should explore the codebase read-only and design an approach "
        "before writing any code. Use ExitPlanMode when ready to present your plan."
    )
    search_hint = "switch to plan mode to design approach before coding"

    @property
    def input_schema(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "plan_text": {
                    "type": "string",
                    "description": "Initial plan text or summary to record",
                },
                "context": {
                    "type": "string",
                    "description": "Context or task description guiding the plan",
                },
            },
            "properties": {},
        }

    def is_enabled(self) -> bool:
        state = _load_plan_state()
        return not state.get("active", False)

    def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
        state = _load_plan_state()
        if state.get("active"):
            return ToolResult(success=False, error="Already in plan mode. Use ExitPlanMode first.")

        now = datetime.now(timezone.utc).isoformat()
        plan_text = input_data.get("plan_text", "")
        context = input_data.get("context", "")

        new_state = {
            "active": True,
            "entered_at": now,
            "plan_text": plan_text,
            "context": context,
            "exited_at": None,
        }
        _save_plan_state(new_state)

        # Initialize reasoning log
        reasoning = _load_reasoning()
        reasoning.append({
            "step": 1,
            "action": "enter_plan_mode",
            "timestamp": now,
            "context": context,
            "note": "Entered plan mode. Begin read-only exploration and design.",
        })
        _save_reasoning(reasoning)

        return ToolResult(
            success=True,
            data={
                "message": "Entered plan mode. Explore the codebase read-only and design your implementation approach.",
                "plan_text": plan_text,
                "context": context,
                "entered_at": now,
            },
        )

    def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
        data = result.get("data", {})
        msg = data.get(
            "message",
            "Entered plan mode. Explore the codebase read-only and design your approach.",
        )
        return f"""{msg}

In plan mode, you should:
1. Explore the codebase to understand existing patterns
2. Identify similar features and architectural approaches
3. Consider multiple approaches and trade-offs
4. Use FileReadTool to understand the structure
5. Design a concrete implementation strategy
6. When ready, use ExitPlanMode to present your plan

DO NOT write or edit any files yet. This is a read-only exploration phase."""


# ── ExitPlanModeTool ────────────────────────────────────────────────────────────


class ExitPlanModeTool(BaseTool[dict[str, Any], dict[str, Any]]):
    """Exit plan mode and return to normal execution.

    Parameters
    ----------
    confirm : bool, optional
        Whether the plan is approved (default: True).
    summary : str, optional
        A summary or the full plan text to save.
    """

    name = "ExitPlanMode"
    description = (
        "Exit plan mode and return to normal execution. "
        "Call this when you have finished your plan and are ready to code, "
        "or to abandon the plan without implementing."
    )
    search_hint = "exit plan mode and start coding present plan for approval"

    @property
    def input_schema(self) -> dict[str, Any]:
        return {
            "type": "object",
            "properties": {
                "confirm": {
                    "type": "boolean",
                    "description": "Whether the plan is approved (default: True)",
                    "default": True,
                },
                "summary": {
                    "type": "string",
                    "description": "Plan summary or full plan text to save",
                },
            },
            "properties": {},
        }

    def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
        state = _load_plan_state()
        if not state.get("active"):
            return ToolResult(success=False, error="Not in plan mode. Use EnterPlanMode first.")

        confirm = input_data.get("confirm", True)
        summary = input_data.get("summary") or state.get("plan_text", "")

        now = datetime.now(timezone.utc).isoformat()

        # Log exit reasoning step
        reasoning = _load_reasoning()
        reasoning.append({
            "step": len(reasoning) + 1,
            "action": "exit_plan_mode",
            "timestamp": now,
            "confirm": confirm,
            "summary_length": len(summary) if summary else 0,
            "note": "Exited plan mode" + (" (plan approved)" if confirm else " (plan rejected/abandoned)"),
        })
        _save_reasoning(reasoning)

        # Update plan state
        new_state = {
            **state,
            "active": False,
            "exited_at": now,
            "plan_text": summary if summary else state.get("plan_text"),
            "approved": confirm,
        }
        _save_plan_state(new_state)

        return ToolResult(
            success=True,
            data={
                "message": "Exited plan mode. Ready to proceed." if confirm else "Plan abandoned.",
                "plan_text": summary,
                "confirmed": confirm,
                "exited_at": now,
            },
        )

    def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
        data = result.get("data", {})
        confirm = data.get("confirmed", True)
        plan_text = data.get("plan_text", "")

        if confirm:
            lines = ["Plan approved. You can now start coding."]
            if plan_text:
                lines.append(f"\nPlan saved:\n{plan_text}")
            return "\n".join(lines)
        else:
            return "Plan abandoned. Exited plan mode."


# Auto-register plan mode tools
get_registry().register(EnterPlanModeTool())
get_registry().register(ExitPlanModeTool())