walidsobhie-code commited on
Commit
359acf6
·
1 Parent(s): 71c6bb8

Fix tool parameter signatures and improve consistency

Browse files
src/tools/config_tool.py CHANGED
@@ -1,172 +1,287 @@
1
- """ConfigTool - Runtime configuration management for Stack 2.9"""
2
 
3
- import json
4
- from pathlib import Path
5
- from typing import Any, Dict, Optional
6
-
7
- from .base import BaseTool, ToolResult
8
- from .registry import tool_registry
9
-
10
- CONFIG_FILE = Path.home() / ".stack-2.9" / "config.json"
11
 
 
 
 
 
 
12
 
13
- def _load_config() -> Dict[str, Any]:
14
- """Load config from disk."""
15
- CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
16
- if CONFIG_FILE.exists():
17
- return json.loads(CONFIG_FILE.read_text())
18
- return {"settings": {}}
19
 
 
20
 
21
- def _save_config(data: Dict[str, Any]) -> None:
22
- """Save config to disk."""
23
- CONFIG_FILE.write_text(json.dumps(data, indent=2))
24
-
25
 
26
- # Default settings
27
- DEFAULT_SETTINGS = {
28
- "model": "Qwen/Qwen2.5-Coder-1.5B",
29
- "temperature": 0.7,
30
- "max_tokens": 2048,
31
- "context_length": 32768,
32
- "tools_enabled": True,
33
- "voice_enabled": False,
34
- "language": "en"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
 
38
- class ConfigGetTool(BaseTool):
39
- """Get a configuration value."""
40
-
41
- name = "config_get"
42
- description = "Get configuration value"
43
-
44
- input_schema = {
45
- "type": "object",
46
- "properties": {
47
- "key": {"type": "string", "description": "Config key"}
48
- },
49
- "required": ["key"]
50
- }
51
-
52
- async def execute(self, key: str) -> ToolResult:
53
- """Get config value."""
54
- data = _load_config()
55
- value = data.get("settings", {}).get(key)
56
-
57
- if value is None:
58
- value = DEFAULT_SETTINGS.get(key)
59
-
60
- if value is None:
61
- return ToolResult(success=False, error=f"Config key not found: {key}")
62
-
63
- return ToolResult(success=True, data={
64
- "key": key,
65
- "value": value
66
- })
67
-
68
-
69
- class ConfigSetTool(BaseTool):
70
- """Set a configuration value."""
71
-
72
- name = "config_set"
73
- description = "Set configuration value"
74
-
75
- input_schema = {
76
- "type": "object",
77
- "properties": {
78
- "key": {"type": "string", "description": "Config key"},
79
- "value": {"type": "string", "description": "Config value"}
80
- },
81
- "required": ["key", "value"]
82
- }
83
-
84
- async def execute(self, key: str, value: Any) -> ToolResult:
85
- """Set config value."""
86
- data = _load_config()
87
-
88
- if "settings" not in data:
89
- data["settings"] = {}
90
-
91
- # Type conversion
92
- if value.lower() in ("true", "false"):
93
- value = value.lower() == "true"
94
- elif value.isdigit():
95
- value = int(value)
96
- elif value.replace(".", "", 1).isdigit():
97
- value = float(value)
98
-
99
- data["settings"][key] = value
100
- _save_config(data)
101
-
102
- return ToolResult(success=True, data={
103
- "key": key,
104
- "value": value,
105
- "status": "set"
106
- })
107
-
108
-
109
- class ConfigListTool(BaseTool):
110
- """List all configuration values."""
111
-
112
- name = "config_list"
113
- description = "List all configuration settings"
114
-
115
- input_schema = {
116
- "type": "object",
117
- "properties": {
118
- "include_defaults": {"type": "boolean", "default": False}
119
- },
120
- "required": []
121
- }
122
-
123
- async def execute(self, include_defaults: bool = False) -> ToolResult:
124
- """List config."""
125
- data = _load_config()
126
- settings = data.get("settings", {})
127
-
128
- if include_defaults:
129
- all_settings = dict(DEFAULT_SETTINGS)
130
- all_settings.update(settings)
131
- settings = all_settings
132
-
133
- return ToolResult(success=True, data={
134
- "settings": settings,
135
- "count": len(settings)
136
- })
137
-
138
-
139
- class ConfigDeleteTool(BaseTool):
140
- """Delete a configuration value (reset to default)."""
141
-
142
- name = "config_delete"
143
- description = "Delete configuration value"
144
-
145
- input_schema = {
146
- "type": "object",
147
- "properties": {
148
- "key": {"type": "string", "description": "Config key to delete"}
149
- },
150
- "required": ["key"]
151
- }
152
-
153
- async def execute(self, key: str) -> ToolResult:
154
- """Delete config."""
155
- data = _load_config()
156
-
157
- if key in data.get("settings", {}):
158
- del data["settings"][key]
159
- _save_config(data)
160
- return ToolResult(success=True, data={
161
  "key": key,
162
- "status": "deleted"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  })
164
 
165
- return ToolResult(success=False, error=f"Config key not found: {key}")
166
-
167
-
168
- # Register tools
169
- tool_registry.register(ConfigGetTool())
170
- tool_registry.register(ConfigSetTool())
171
- tool_registry.register(ConfigListTool())
172
- tool_registry.register(ConfigDeleteTool())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ConfigTool runtime configuration management for Stack 2.9.
2
 
3
+ Stores configuration in ~/.stack-2.9/config.json
 
 
 
 
 
 
 
4
 
5
+ Operations:
6
+ - get : retrieve a configuration value
7
+ - set : set a configuration value
8
+ - list : list all configuration keys and values
9
+ - delete : remove a configuration key
10
 
11
+ Input schema:
12
+ operation : str — one of get, set, list, delete
13
+ key : str — configuration key (required for get/set/delete)
14
+ value : any — new value (required for set)
15
+ """
 
16
 
17
+ from __future__ import annotations
18
 
19
+ import json
20
+ import os
21
+ from typing import Any
 
22
 
23
+ from .base import BaseTool, ToolResult
24
+ from .registry import get_registry
25
+
26
+ TOOL_NAME = "Config"
27
+ DATA_DIR = os.path.expanduser("~/.stack-2.9")
28
+ CONFIG_FILE = os.path.join(DATA_DIR, "config.json")
29
+
30
+
31
+ def _load_config() -> dict[str, Any]:
32
+ os.makedirs(DATA_DIR, exist_ok=True)
33
+ if os.path.exists(CONFIG_FILE):
34
+ try:
35
+ with open(CONFIG_FILE) as f:
36
+ return json.load(f)
37
+ except Exception:
38
+ pass
39
+ return {}
40
+
41
+
42
+ def _save_config(config: dict[str, Any]) -> None:
43
+ os.makedirs(DATA_DIR, exist_ok=True)
44
+ with open(CONFIG_FILE, "w") as f:
45
+ json.dump(config, f, indent=2, default=str)
46
+
47
+
48
+ # Supported configuration keys and their metadata
49
+ SUPPORTED_KEYS: dict[str, dict[str, Any]] = {
50
+ "theme": {
51
+ "type": "string",
52
+ "options": ["light", "dark", "system"],
53
+ "default": "system",
54
+ "description": "UI theme",
55
+ },
56
+ "model": {
57
+ "type": "string",
58
+ "description": "Default model to use",
59
+ "default": "",
60
+ },
61
+ "max_tokens": {
62
+ "type": "number",
63
+ "description": "Maximum tokens per response",
64
+ "default": 4096,
65
+ },
66
+ "temperature": {
67
+ "type": "number",
68
+ "description": "Sampling temperature",
69
+ "default": 0.7,
70
+ },
71
+ "verbose": {
72
+ "type": "boolean",
73
+ "description": "Enable verbose output",
74
+ "default": False,
75
+ },
76
+ "permissions.defaultMode": {
77
+ "type": "string",
78
+ "options": ["auto", "plan", "bypass"],
79
+ "description": "Default permissions mode",
80
+ "default": "auto",
81
+ },
82
+ "tools.enabled": {
83
+ "type": "array",
84
+ "items": {"type": "string"},
85
+ "description": "List of enabled tool names",
86
+ "default": [],
87
+ },
88
+ "tools.disabled": {
89
+ "type": "array",
90
+ "items": {"type": "string"},
91
+ "description": "List of disabled tool names",
92
+ "default": [],
93
+ },
94
  }
95
 
96
 
97
+ class ConfigTool(BaseTool[dict[str, Any], dict[str, Any]]):
98
+ """Runtime configuration management tool.
99
+
100
+ Supports get, set, list, and delete operations on the persistent config store.
101
+ """
102
+
103
+ name = TOOL_NAME
104
+ description = "Get, set, list, or delete Stack 2.9 runtime configuration settings."
105
+ search_hint = "get or set configuration settings theme model permissions"
106
+
107
+ @property
108
+ def input_schema(self) -> dict[str, Any]:
109
+ return {
110
+ "type": "object",
111
+ "properties": {
112
+ "operation": {
113
+ "type": "string",
114
+ "enum": ["get", "set", "list", "delete"],
115
+ "description": "Configuration operation",
116
+ },
117
+ "key": {
118
+ "type": "string",
119
+ "description": "Configuration key (required for get/set/delete)",
120
+ },
121
+ "value": {
122
+ "description": "New value (required for 'set' operation)",
123
+ },
124
+ },
125
+ "required": ["operation"],
126
+ }
127
+
128
+ def validate_input(self, input_data: dict[str, Any]) -> tuple[bool, str | None]:
129
+ op = input_data.get("operation")
130
+ if op in ("get", "set", "delete") and not input_data.get("key"):
131
+ return False, f"Error: 'key' is required for '{op}' operation"
132
+ if op == "set" and "value" not in input_data:
133
+ return False, "Error: 'value' is required for 'set' operation"
134
+ return True, None
135
+
136
+ def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
137
+ op = input_data.get("operation")
138
+ key = input_data.get("key")
139
+ value = input_data.get("value")
140
+
141
+ if op == "get":
142
+ return self._get(key)
143
+ elif op == "set":
144
+ return self._set(key, value)
145
+ elif op == "list":
146
+ return self._list()
147
+ elif op == "delete":
148
+ return self._delete(key)
149
+ else:
150
+ return ToolResult(success=False, error=f"Unknown operation: {op}")
151
+
152
+ def _get(self, key: str | None) -> ToolResult[dict[str, Any]]:
153
+ if key is None:
154
+ return ToolResult(success=False, error="Key is required for 'get'")
155
+ config = _load_config()
156
+ current = config.get(key)
157
+ meta = SUPPORTED_KEYS.get(key, {})
158
+ return ToolResult(
159
+ success=True,
160
+ data={
161
+ "operation": "get",
162
+ "key": key,
163
+ "value": current if current is not None else meta.get("default"),
164
+ "default": meta.get("default"),
165
+ },
166
+ )
167
+
168
+ def _set(self, key: str | None, value: Any) -> ToolResult[dict[str, Any]]:
169
+ if key is None:
170
+ return ToolResult(success=False, error="Key is required for 'set'")
171
+
172
+ meta = SUPPORTED_KEYS.get(key)
173
+ if meta is not None:
174
+ expected_type = meta.get("type")
175
+ # Type coercion
176
+ if expected_type == "boolean":
177
+ if isinstance(value, str):
178
+ value = value.lower() in ("true", "1", "yes")
179
+ elif expected_type == "number":
180
+ try:
181
+ value = float(value)
182
+ except (ValueError, TypeError):
183
+ return ToolResult(
184
+ success=False,
185
+ error=f"Invalid number value for '{key}': {value}",
186
+ )
187
+ # Validate options
188
+ options = meta.get("options")
189
+ if options and value not in options:
190
+ return ToolResult(
191
+ success=False,
192
+ error=f"Invalid value '{value}' for '{key}'. Options: {', '.join(options)}",
193
+ )
194
+
195
+ config = _load_config()
196
+ previous = config.get(key)
197
+ config[key] = value
198
+ _save_config(config)
199
+
200
+ return ToolResult(
201
+ success=True,
202
+ data={
203
+ "operation": "set",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  "key": key,
205
+ "previousValue": previous,
206
+ "newValue": value,
207
+ },
208
+ )
209
+
210
+ def _list(self) -> ToolResult[dict[str, Any]]:
211
+ config = _load_config()
212
+ meta = SUPPORTED_KEYS
213
+
214
+ items = []
215
+ all_keys = sorted(set(list(config.keys()) + list(meta.keys())))
216
+ for k in all_keys:
217
+ items.append({
218
+ "key": k,
219
+ "value": config.get(k, meta.get(k, {}).get("default")),
220
+ "description": meta.get(k, {}).get("description", ""),
221
+ "type": meta.get(k, {}).get("type", "unknown"),
222
+ "options": meta.get(k, {}).get("options"),
223
+ "is_default": k not in config,
224
  })
225
 
226
+ return ToolResult(
227
+ success=True,
228
+ data={
229
+ "operation": "list",
230
+ "settings": items,
231
+ "total": len(items),
232
+ },
233
+ )
234
+
235
+ def _delete(self, key: str | None) -> ToolResult[dict[str, Any]]:
236
+ if key is None:
237
+ return ToolResult(success=False, error="Key is required for 'delete'")
238
+ config = _load_config()
239
+ if key not in config:
240
+ return ToolResult(success=False, error=f"Key '{key}' not found in config")
241
+ previous = config.pop(key)
242
+ _save_config(config)
243
+ return ToolResult(
244
+ success=True,
245
+ data={
246
+ "operation": "delete",
247
+ "key": key,
248
+ "previousValue": previous,
249
+ },
250
+ )
251
+
252
+ def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
253
+ if not result.get("success", True):
254
+ return f"Error: {result.get('error')}"
255
+ data = result.get("data", {})
256
+ op = data.get("operation", "")
257
+
258
+ if op == "get":
259
+ val = data.get("value")
260
+ default = data.get("default")
261
+ note = f" (default: {default})" if default is not None and val == default else ""
262
+ return f"{data['key']} = {json.dumps(val)}{note}"
263
+
264
+ elif op == "set":
265
+ return f"Set {data['key']} = {json.dumps(data['newValue'])}"
266
+
267
+ elif op == "list":
268
+ settings = data.get("settings", [])
269
+ if not settings:
270
+ return "No configuration settings found."
271
+ lines = [f"{data['total']} setting(s):\n"]
272
+ for s in settings:
273
+ val = json.dumps(s["value"])
274
+ note = f" [{s['type']}]" if s["type"] != "unknown" else ""
275
+ if s["is_default"]:
276
+ note += " (default)"
277
+ lines.append(f" {s['key']:30} = {val}{note}")
278
+ return "\n".join(lines)
279
+
280
+ elif op == "delete":
281
+ return f"Deleted {data['key']} (was: {json.dumps(data['previousValue'])})"
282
+
283
+ return str(data)
284
+
285
+
286
+ # Auto-register
287
+ get_registry().register(ConfigTool())
src/tools/plan_mode.py CHANGED
@@ -1,171 +1,261 @@
1
- """PlanModeTool - Agent reasoning mode for Stack 2.9"""
2
-
3
- import json
4
- from datetime import datetime
5
- from pathlib import Path
6
- from typing import Any, Dict, List, Optional
7
-
8
- from .base import BaseTool, ToolResult
9
- from .registry import tool_registry
10
-
11
- PLAN_FILE = Path.home() / ".stack-2.9" / "plan.json"
12
 
 
 
13
 
14
- def _load_plan() -> Dict[str, Any]:
15
- """Load current plan state."""
16
- PLAN_FILE.parent.mkdir(parents=True, exist_ok=True)
17
- if PLAN_FILE.exists():
18
- return json.loads(PLAN_FILE.read_text())
19
- return {"active": False, "steps": [], "context": ""}
20
 
 
 
 
21
 
22
- def _save_plan(data: Dict[str, Any]) -> None:
23
- """Save plan state."""
24
- PLAN_FILE.write_text(json.dumps(data, indent=2))
25
 
 
 
 
 
26
 
27
- def _clear_plan() -> None:
28
- """Clear plan state."""
29
- if PLAN_FILE.exists():
30
- PLAN_FILE.unlink()
31
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
 
33
- class EnterPlanModeTool(BaseTool):
34
- """Enter plan mode for structured reasoning."""
 
35
 
36
- name = "enter_plan_mode"
37
- description = "Enter plan mode for structured reasoning"
 
 
38
 
39
- input_schema = {
40
- "type": "object",
41
- "properties": {
42
- "context": {"type": "string", "description": "Initial context or goal"},
43
- "steps": {"type": "array", "items": {"type": "string"}, "description": "Initial reasoning steps"}
44
- },
45
- "required": ["context"]
46
- }
47
 
48
- async def execute(self, context: str, steps: Optional[List[str]] = None) -> ToolResult:
49
- """Enter plan mode."""
50
- data = {
51
  "active": True,
 
 
52
  "context": context,
53
- "steps": steps or [],
54
- "entered_at": datetime.now().isoformat(),
55
- "last_updated": datetime.now().isoformat()
56
  }
57
-
58
- _save_plan(data)
59
-
60
- return ToolResult(success=True, data={
61
- "status": "entered",
 
 
 
62
  "context": context,
63
- "step_count": len(steps or [])
64
  })
65
-
66
-
67
- class ExitPlanModeTool(BaseTool):
68
- """Exit plan mode."""
69
-
70
- name = "exit_plan_mode"
71
- description = "Exit plan mode and get summary"
72
-
73
- input_schema = {
74
- "type": "object",
75
- "properties": {
76
- "confirm": {"type": "boolean", "default": False, "description": "Confirm exit"},
77
- "summary": {"type": "string", "description": "Optional summary of reasoning"}
78
- },
79
- "required": ["confirm"]
80
- }
81
-
82
- async def execute(self, confirm: bool, summary: Optional[str] = None) -> ToolResult:
83
- """Exit plan mode."""
84
- if not confirm:
85
- return ToolResult(success=False, error="Must confirm exit to leave plan mode")
86
-
87
- data = _load_plan()
88
-
89
- if not data.get("active"):
90
- return ToolResult(success=False, error="Plan mode not active")
91
-
92
- result = {
93
- "status": "exited",
94
- "context": data.get("context"),
95
- "step_count": len(data.get("steps", [])),
96
- "summary": summary or data.get("context", ""),
97
- "duration": datetime.now().isoformat()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98
  }
99
 
100
- _clear_plan()
101
-
102
- return ToolResult(success=True, data=result)
103
-
104
-
105
- class PlanAddStepTool(BaseTool):
106
- """Add a reasoning step to the plan."""
107
-
108
- name = "plan_add_step"
109
- description = "Add a reasoning step to active plan"
110
-
111
- input_schema = {
112
- "type": "object",
113
- "properties": {
114
- "step": {"type": "string", "description": "Reasoning step text"},
115
- "confidence": {"type": "number", "description": "Confidence level 0-1"}
116
- },
117
- "required": ["step"]
118
- }
119
-
120
- async def execute(self, step: str, confidence: Optional[float] = None) -> ToolResult:
121
- """Add step to plan."""
122
- data = _load_plan()
123
-
124
- if not data.get("active"):
125
- return ToolResult(success=False, error="Plan mode not active")
126
-
127
- step_entry = {
128
- "text": step,
129
- "timestamp": datetime.now().isoformat(),
130
- "confidence": confidence
131
- }
132
-
133
- data["steps"].append(step_entry)
134
- data["last_updated"] = datetime.now().isoformat()
135
- _save_plan(data)
136
-
137
- return ToolResult(success=True, data={
138
- "step_number": len(data["steps"]),
139
- "total_steps": len(data["steps"])
140
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
141
 
142
-
143
- class PlanStatusTool(BaseTool):
144
- """Get current plan status."""
145
-
146
- name = "plan_status"
147
- description = "Get status of current plan"
148
-
149
- input_schema = {
150
- "type": "object",
151
- "properties": {},
152
- "required": []
153
- }
154
-
155
- async def execute(self) -> ToolResult:
156
- """Get plan status."""
157
- data = _load_plan()
158
-
159
- return ToolResult(success=True, data={
160
- "active": data.get("active", False),
161
- "context": data.get("context"),
162
- "steps": data.get("steps", []),
163
- "step_count": len(data.get("steps", []))
164
- })
165
-
166
-
167
- # Register tools
168
- tool_registry.register(EnterPlanModeTool())
169
- tool_registry.register(ExitPlanModeTool())
170
- tool_registry.register(PlanAddStepTool())
171
- tool_registry.register(PlanStatusTool())
 
1
+ """Plan mode tools EnterPlanMode and ExitPlanMode.
 
 
 
 
 
 
 
 
 
 
2
 
3
+ EnterPlanMode switches the agent into a reasoning/planning mode where it
4
+ explores the codebase read-only before writing any code.
5
 
6
+ ExitPlanMode exits planning mode and returns to normal execution mode.
 
 
 
 
 
7
 
8
+ Plan state is stored in ~/.stack-2.9/plan_mode.json
9
+ Reasoning steps are tracked in ~/.stack-2.9/plan_reasoning.json
10
+ """
11
 
12
+ from __future__ import annotations
 
 
13
 
14
+ import json
15
+ import os
16
+ from datetime import datetime, timezone
17
+ from typing import Any
18
 
19
+ from .base import BaseTool, ToolResult
20
+ from .registry import get_registry
21
+
22
+ DATA_DIR = os.path.expanduser("~/.stack-2.9")
23
+ PLAN_STATE_FILE = os.path.join(DATA_DIR, "plan_mode.json")
24
+ REASONING_FILE = os.path.join(DATA_DIR, "plan_reasoning.json")
25
+
26
+
27
+ def _load_plan_state() -> dict[str, Any]:
28
+ os.makedirs(DATA_DIR, exist_ok=True)
29
+ if os.path.exists(PLAN_STATE_FILE):
30
+ try:
31
+ with open(PLAN_STATE_FILE) as f:
32
+ return json.load(f)
33
+ except Exception:
34
+ pass
35
+ return {"active": False, "entered_at": None, "plan_text": None, "context": None}
36
+
37
+
38
+ def _save_plan_state(state: dict[str, Any]) -> None:
39
+ os.makedirs(DATA_DIR, exist_ok=True)
40
+ with open(PLAN_STATE_FILE, "w") as f:
41
+ json.dump(state, f, indent=2, default=str)
42
+
43
+
44
+ def _load_reasoning() -> list[dict[str, Any]]:
45
+ os.makedirs(DATA_DIR, exist_ok=True)
46
+ if os.path.exists(REASONING_FILE):
47
+ try:
48
+ with open(REASONING_FILE) as f:
49
+ return json.load(f)
50
+ except Exception:
51
+ pass
52
+ return []
53
+
54
+
55
+ def _save_reasoning(steps: list[dict[str, Any]]) -> None:
56
+ os.makedirs(DATA_DIR, exist_ok=True)
57
+ with open(REASONING_FILE, "w") as f:
58
+ json.dump(steps, f, indent=2, default=str)
59
+
60
+
61
+ # ── EnterPlanModeTool ───────────────────────────────────────────────────────────
62
+
63
+
64
+ class EnterPlanModeTool(BaseTool[dict[str, Any], dict[str, Any]]):
65
+ """Enter plan mode — a read-only reasoning phase for exploring and designing.
66
+
67
+ Parameters
68
+ ----------
69
+ plan_text : str, optional
70
+ Initial plan text to record.
71
+ context : str, optional
72
+ Context or task description for the plan.
73
+ """
74
+
75
+ name = "EnterPlanMode"
76
+ description = (
77
+ "Switch to plan mode for complex tasks requiring exploration and design. "
78
+ "In plan mode, you should explore the codebase read-only and design an approach "
79
+ "before writing any code. Use ExitPlanMode when ready to present your plan."
80
+ )
81
+ search_hint = "switch to plan mode to design approach before coding"
82
+
83
+ @property
84
+ def input_schema(self) -> dict[str, Any]:
85
+ return {
86
+ "type": "object",
87
+ "properties": {
88
+ "plan_text": {
89
+ "type": "string",
90
+ "description": "Initial plan text or summary to record",
91
+ },
92
+ "context": {
93
+ "type": "string",
94
+ "description": "Context or task description guiding the plan",
95
+ },
96
+ },
97
+ "properties": {},
98
+ }
99
 
100
+ def is_enabled(self) -> bool:
101
+ state = _load_plan_state()
102
+ return not state.get("active", False)
103
 
104
+ def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
105
+ state = _load_plan_state()
106
+ if state.get("active"):
107
+ return ToolResult(success=False, error="Already in plan mode. Use ExitPlanMode first.")
108
 
109
+ now = datetime.now(timezone.utc).isoformat()
110
+ plan_text = input_data.get("plan_text", "")
111
+ context = input_data.get("context", "")
 
 
 
 
 
112
 
113
+ new_state = {
 
 
114
  "active": True,
115
+ "entered_at": now,
116
+ "plan_text": plan_text,
117
  "context": context,
118
+ "exited_at": None,
 
 
119
  }
120
+ _save_plan_state(new_state)
121
+
122
+ # Initialize reasoning log
123
+ reasoning = _load_reasoning()
124
+ reasoning.append({
125
+ "step": 1,
126
+ "action": "enter_plan_mode",
127
+ "timestamp": now,
128
  "context": context,
129
+ "note": "Entered plan mode. Begin read-only exploration and design.",
130
  })
131
+ _save_reasoning(reasoning)
132
+
133
+ return ToolResult(
134
+ success=True,
135
+ data={
136
+ "message": "Entered plan mode. Explore the codebase read-only and design your implementation approach.",
137
+ "plan_text": plan_text,
138
+ "context": context,
139
+ "entered_at": now,
140
+ },
141
+ )
142
+
143
+ def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
144
+ data = result.get("data", {})
145
+ msg = data.get(
146
+ "message",
147
+ "Entered plan mode. Explore the codebase read-only and design your approach.",
148
+ )
149
+ return f"""{msg}
150
+
151
+ In plan mode, you should:
152
+ 1. Explore the codebase to understand existing patterns
153
+ 2. Identify similar features and architectural approaches
154
+ 3. Consider multiple approaches and trade-offs
155
+ 4. Use FileReadTool to understand the structure
156
+ 5. Design a concrete implementation strategy
157
+ 6. When ready, use ExitPlanMode to present your plan
158
+
159
+ DO NOT write or edit any files yet. This is a read-only exploration phase."""
160
+
161
+
162
+ # ── ExitPlanModeTool ────────────────────────────────────────────────────────────
163
+
164
+
165
+ class ExitPlanModeTool(BaseTool[dict[str, Any], dict[str, Any]]):
166
+ """Exit plan mode and return to normal execution.
167
+
168
+ Parameters
169
+ ----------
170
+ confirm : bool, optional
171
+ Whether the plan is approved (default: True).
172
+ summary : str, optional
173
+ A summary or the full plan text to save.
174
+ """
175
+
176
+ name = "ExitPlanMode"
177
+ description = (
178
+ "Exit plan mode and return to normal execution. "
179
+ "Call this when you have finished your plan and are ready to code, "
180
+ "or to abandon the plan without implementing."
181
+ )
182
+ search_hint = "exit plan mode and start coding present plan for approval"
183
+
184
+ @property
185
+ def input_schema(self) -> dict[str, Any]:
186
+ return {
187
+ "type": "object",
188
+ "properties": {
189
+ "confirm": {
190
+ "type": "boolean",
191
+ "description": "Whether the plan is approved (default: True)",
192
+ "default": True,
193
+ },
194
+ "summary": {
195
+ "type": "string",
196
+ "description": "Plan summary or full plan text to save",
197
+ },
198
+ },
199
+ "properties": {},
200
  }
201
 
202
+ def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
203
+ state = _load_plan_state()
204
+ if not state.get("active"):
205
+ return ToolResult(success=False, error="Not in plan mode. Use EnterPlanMode first.")
206
+
207
+ confirm = input_data.get("confirm", True)
208
+ summary = input_data.get("summary") or state.get("plan_text", "")
209
+
210
+ now = datetime.now(timezone.utc).isoformat()
211
+
212
+ # Log exit reasoning step
213
+ reasoning = _load_reasoning()
214
+ reasoning.append({
215
+ "step": len(reasoning) + 1,
216
+ "action": "exit_plan_mode",
217
+ "timestamp": now,
218
+ "confirm": confirm,
219
+ "summary_length": len(summary) if summary else 0,
220
+ "note": "Exited plan mode" + (" (plan approved)" if confirm else " (plan rejected/abandoned)"),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  })
222
+ _save_reasoning(reasoning)
223
+
224
+ # Update plan state
225
+ new_state = {
226
+ **state,
227
+ "active": False,
228
+ "exited_at": now,
229
+ "plan_text": summary if summary else state.get("plan_text"),
230
+ "approved": confirm,
231
+ }
232
+ _save_plan_state(new_state)
233
+
234
+ return ToolResult(
235
+ success=True,
236
+ data={
237
+ "message": "Exited plan mode. Ready to proceed." if confirm else "Plan abandoned.",
238
+ "plan_text": summary,
239
+ "confirmed": confirm,
240
+ "exited_at": now,
241
+ },
242
+ )
243
+
244
+ def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
245
+ data = result.get("data", {})
246
+ confirm = data.get("confirmed", True)
247
+ plan_text = data.get("plan_text", "")
248
+
249
+ if confirm:
250
+ lines = ["Plan approved. You can now start coding."]
251
+ if plan_text:
252
+ lines.append(f"\nPlan saved:\n{plan_text}")
253
+ return "\n".join(lines)
254
+ else:
255
+ return "Plan abandoned. Exited plan mode."
256
+
257
+
258
+ # Auto-register plan mode tools
259
+ get_registry().register(EnterPlanModeTool())
260
+ get_registry().register(ExitPlanModeTool())
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tools/registry.py CHANGED
@@ -51,3 +51,6 @@ class ToolRegistry:
51
  def get_registry() -> ToolRegistry:
52
  """Get the global ToolRegistry instance."""
53
  return ToolRegistry()
 
 
 
 
51
  def get_registry() -> ToolRegistry:
52
  """Get the global ToolRegistry instance."""
53
  return ToolRegistry()
54
+
55
+ # Global registry instance
56
+ tool_registry = ToolRegistry()
src/tools/todo_tool.py CHANGED
@@ -1,169 +1,184 @@
1
- """TodoWriteTool - Persistent todo lists for Stack 2.9"""
2
 
3
- import json
4
- import uuid
5
- from datetime import datetime
6
- from pathlib import Path
7
- from typing import Any, Dict, List, Optional
8
-
9
- from .base import BaseTool, ToolResult
10
- from .registry import tool_registry
11
-
12
- TODOS_FILE = Path.home() / ".stack-2.9" / "todos.json"
13
 
 
 
 
 
 
14
 
15
- def _load_todos() -> Dict[str, Any]:
16
- """Load todos from disk."""
17
- TODOS_FILE.parent.mkdir(parents=True, exist_ok=True)
18
- if TODOS_FILE.exists():
19
- return json.loads(TODOS_FILE.read_text())
20
- return {"todos": []}
21
 
 
22
 
23
- def _save_todos(data: Dict[str, Any]) -> None:
24
- """Save todos to disk."""
25
- TODOS_FILE.write_text(json.dumps(data, indent=2))
26
-
27
-
28
- class TodoAddTool(BaseTool):
29
- """Add a new todo item."""
30
-
31
- name = "todo_add"
32
- description = "Add a new todo item"
33
-
34
- input_schema = {
35
- "type": "object",
36
- "properties": {
37
- "task": {"type": "string", "description": "Todo task description"},
38
- "priority": {"type": "string", "enum": ["low", "medium", "high"], "default": "medium"},
39
- "tags": {"type": "array", "items": {"type": "string"}, "description": "Optional tags"}
40
- },
41
- "required": ["task"]
42
- }
43
 
44
- async def execute(self, task: str, priority: str = "medium", tags: Optional[List[str]] = None) -> ToolResult:
45
- """Add todo."""
46
- data = _load_todos()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  todo_id = str(uuid.uuid4())[:8]
49
- todo = {
 
50
  "id": todo_id,
51
- "task": task,
52
- "priority": priority,
53
  "status": "pending",
54
- "tags": tags or [],
55
- "created_at": datetime.now().isoformat()
 
56
  }
57
-
58
- data["todos"].append(todo)
59
- _save_todos(data)
60
-
61
- return ToolResult(success=True, data={
62
- "id": todo_id,
63
- "task": task,
64
- "status": "added"
65
- })
66
-
67
-
68
- class TodoListTool(BaseTool):
69
- """List all todos."""
70
-
71
- name = "todo_list"
72
- description = "List all todos with optional filters"
73
-
74
- input_schema = {
75
- "type": "object",
76
- "properties": {
77
- "status": {"type": "string", "enum": ["pending", "completed", "all"], "default": "pending"},
78
- "tag": {"type": "string", "description": "Filter by tag"},
79
- "priority": {"type": "string", "enum": ["low", "medium", "high"]}
80
- },
81
- "required": []
82
- }
83
-
84
- async def execute(self, status: str = "pending", tag: Optional[str] = None, priority: Optional[str] = None) -> ToolResult:
85
- """List todos."""
86
- data = _load_todos()
87
- todos = data.get("todos", [])
88
-
89
- if status != "all":
90
- todos = [t for t in todos if t.get("status") == status]
91
-
92
- if tag:
93
- todos = [t for t in todos if tag in t.get("tags", [])]
94
-
95
- if priority:
96
- todos = [t for t in todos if t.get("priority") == priority]
97
-
98
- return ToolResult(success=True, data={
99
- "todos": todos,
100
- "count": len(todos)
101
- })
102
-
103
-
104
- class TodoCompleteTool(BaseTool):
105
- """Mark a todo as completed."""
106
-
107
- name = "todo_complete"
108
- description = "Mark a todo as completed"
109
-
110
- input_schema = {
111
- "type": "object",
112
- "properties": {
113
- "todo_id": {"type": "string", "description": "Todo ID to complete"}
114
- },
115
- "required": ["todo_id"]
116
- }
117
-
118
- async def execute(self, todo_id: str) -> ToolResult:
119
- """Complete todo."""
120
- data = _load_todos()
121
-
122
- for todo in data["todos"]:
123
- if todo["id"] == todo_id:
124
- todo["status"] = "completed"
125
- todo["completed_at"] = datetime.now().isoformat()
126
- _save_todos(data)
127
- return ToolResult(success=True, data={
128
- "id": todo_id,
129
- "status": "completed"
130
- })
131
-
132
- return ToolResult(success=False, error=f"Todo {todo_id} not found")
133
-
134
-
135
- class TodoDeleteTool(BaseTool):
136
- """Delete a todo."""
137
-
138
- name = "todo_delete"
139
- description = "Delete a todo item"
140
-
141
- input_schema = {
142
- "type": "object",
143
- "properties": {
144
- "todo_id": {"type": "string", "description": "Todo ID to delete"}
145
- },
146
- "required": ["todo_id"]
147
- }
148
-
149
- async def execute(self, todo_id: str) -> ToolResult:
150
- """Delete todo."""
151
- data = _load_todos()
152
- original_count = len(data["todos"])
153
- data["todos"] = [t for t in data["todos"] if t["id"] != todo_id]
154
-
155
- if len(data["todos"]) == original_count:
156
- return ToolResult(success=False, error=f"Todo {todo_id} not found")
157
-
158
- _save_todos(data)
159
- return ToolResult(success=True, data={
160
- "id": todo_id,
161
- "status": "deleted"
162
- })
163
-
164
-
165
- # Register tools
166
- tool_registry.register(TodoAddTool())
167
- tool_registry.register(TodoListTool())
168
- tool_registry.register(TodoCompleteTool())
169
- tool_registry.register(TodoDeleteTool())
 
1
+ """TodoWriteTool persistent todo list management.
2
 
3
+ Stores todos in ~/.stack-2.9/todos.json
 
 
 
 
 
 
 
 
 
4
 
5
+ Operations:
6
+ - add : add a new todo item
7
+ - complete: mark a todo as completed
8
+ - delete : remove a todo by id
9
+ - list : list all todos (optionally filtered)
10
 
11
+ Input schema:
12
+ operation : str — one of add, complete, delete, list
13
+ task : str — description of the task (required for add)
14
+ todo_id : str — id of the todo (required for complete/delete)
15
+ priority : str — low|medium|high|urgent (default: medium, for add)
16
+ """
17
 
18
+ from __future__ import annotations
19
 
20
+ import json
21
+ import os
22
+ import uuid
23
+ from dataclasses import dataclass
24
+ from datetime import datetime, timezone
25
+ from typing import Any
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ from .base import BaseTool, ToolResult
28
+ from .registry import get_registry
29
+
30
+ TOOL_NAME = "TodoWrite"
31
+ DATA_DIR = os.path.expanduser("~/.stack-2.9")
32
+ TODOS_FILE = os.path.join(DATA_DIR, "todos.json")
33
+
34
+
35
+ def _load_todos() -> list[dict[str, Any]]:
36
+ os.makedirs(DATA_DIR, exist_ok=True)
37
+ if os.path.exists(TODOS_FILE):
38
+ try:
39
+ with open(TODOS_FILE) as f:
40
+ return json.load(f)
41
+ except Exception:
42
+ pass
43
+ return []
44
+
45
+
46
+ def _save_todos(todos: list[dict[str, Any]]) -> None:
47
+ os.makedirs(DATA_DIR, exist_ok=True)
48
+ with open(TODOS_FILE, "w") as f:
49
+ json.dump(todos, f, indent=2, default=str)
50
+
51
+
52
+ class TodoWriteTool(BaseTool[dict[str, Any], dict[str, Any]]):
53
+ """Persistent todo list tool supporting add, complete, delete, and list operations."""
54
+
55
+ name = TOOL_NAME
56
+ description = "Manage a persistent session todo list: add, complete, delete, or list items."
57
+ search_hint = "manage session todo checklist add complete delete"
58
+
59
+ @property
60
+ def input_schema(self) -> dict[str, Any]:
61
+ return {
62
+ "type": "object",
63
+ "properties": {
64
+ "operation": {
65
+ "type": "string",
66
+ "enum": ["add", "complete", "delete", "list"],
67
+ "description": "Operation to perform",
68
+ },
69
+ "task": {
70
+ "type": "string",
71
+ "description": "Task description (required for 'add' operation)",
72
+ },
73
+ "todo_id": {
74
+ "type": "string",
75
+ "description": "Todo ID (required for 'complete' and 'delete' operations)",
76
+ },
77
+ "priority": {
78
+ "type": "string",
79
+ "enum": ["low", "medium", "high", "urgent"],
80
+ "description": "Priority level for 'add' operation (default: medium)",
81
+ "default": "medium",
82
+ },
83
+ },
84
+ "required": ["operation"],
85
+ }
86
 
87
+ def validate_input(self, input_data: dict[str, Any]) -> tuple[bool, str | None]:
88
+ op = input_data.get("operation")
89
+ if op == "add" and not input_data.get("task"):
90
+ return False, "Error: 'task' is required when adding a todo"
91
+ if op in ("complete", "delete") and not input_data.get("todo_id"):
92
+ return False, f"Error: 'todo_id' is required for '{op}' operation"
93
+ return True, None
94
+
95
+ def execute(self, input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
96
+ op = input_data.get("operation")
97
+ todos = _load_todos()
98
+
99
+ if op == "add":
100
+ return self._add(todos, input_data)
101
+ elif op == "complete":
102
+ return self._complete(todos, input_data)
103
+ elif op == "delete":
104
+ return self._delete(todos, input_data)
105
+ elif op == "list":
106
+ return self._list(todos, input_data)
107
+ else:
108
+ return ToolResult(success=False, error=f"Unknown operation: {op}")
109
+
110
+ def _add(self, todos: list[dict[str, Any]], input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
111
  todo_id = str(uuid.uuid4())[:8]
112
+ now = datetime.now(timezone.utc).isoformat()
113
+ item = {
114
  "id": todo_id,
115
+ "content": input_data["task"],
 
116
  "status": "pending",
117
+ "priority": input_data.get("priority", "medium"),
118
+ "created_at": now,
119
+ "updated_at": now,
120
  }
121
+ todos.append(item)
122
+ _save_todos(todos)
123
+ return ToolResult(
124
+ success=True,
125
+ data={"id": todo_id, "content": item["content"], "status": "pending", "priority": item["priority"]},
126
+ )
127
+
128
+ def _complete(self, todos: list[dict[str, Any]], input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
129
+ todo_id = input_data["todo_id"]
130
+ for t in todos:
131
+ if t["id"] == todo_id:
132
+ t["status"] = "completed"
133
+ t["updated_at"] = datetime.now(timezone.utc).isoformat()
134
+ _save_todos(todos)
135
+ return ToolResult(success=True, data={"id": todo_id, "status": "completed"})
136
+ return ToolResult(success=False, error=f"Todo #{todo_id} not found")
137
+
138
+ def _delete(self, todos: list[dict[str, Any]], input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
139
+ todo_id = input_data["todo_id"]
140
+ original_len = len(todos)
141
+ todos[:] = [t for t in todos if t["id"] != todo_id]
142
+ if len(todos) == original_len:
143
+ return ToolResult(success=False, error=f"Todo #{todo_id} not found")
144
+ _save_todos(todos)
145
+ return ToolResult(success=True, data={"id": todo_id, "deleted": True})
146
+
147
+ def _list(self, todos: list[dict[str, Any]], input_data: dict[str, Any]) -> ToolResult[dict[str, Any]]:
148
+ status_filter = input_data.get("status")
149
+ if status_filter:
150
+ todos = [t for t in todos if t.get("status") == status_filter]
151
+ return ToolResult(
152
+ success=True,
153
+ data={
154
+ "todos": todos,
155
+ "total": len(todos),
156
+ "pending": sum(1 for t in _load_todos() if t.get("status") == "pending"),
157
+ "completed": sum(1 for t in _load_todos() if t.get("status") == "completed"),
158
+ },
159
+ )
160
+
161
+ def map_result_to_message(self, result: dict, tool_use_id: str | None = None) -> str:
162
+ if "error" in result and not result.get("success", True):
163
+ return result["error"]
164
+ data = result.get("data", {})
165
+ op = data.get("operation", "")
166
+ if op == "add":
167
+ return f"Todo #{data['id']} added: {data['content']} [{data['status']}]"
168
+ elif op == "complete":
169
+ return f"Todo #{data['id']} marked as completed."
170
+ elif op == "delete":
171
+ return f"Todo #{data['id']} deleted."
172
+ elif op == "list":
173
+ items = data.get("todos", [])
174
+ if not items:
175
+ return "No todos found."
176
+ lines = [f"{data['total']} todo(s) (pending: {data['pending']}, completed: {data['completed']}):\n"]
177
+ for t in items:
178
+ lines.append(f" [{t['status']:9}] #{t['id']} [{t.get('priority','medium'):6}] {t['content']}")
179
+ return "\n".join(lines)
180
+ return str(data)
181
+
182
+
183
+ # Auto-register
184
+ get_registry().register(TodoWriteTool())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/tools/tool_discovery.py CHANGED
@@ -4,7 +4,7 @@ import json
4
  from datetime import datetime
5
  from typing import Any, Dict, List
6
 
7
- from .base import ToolResult
8
  from .registry import tool_registry
9
 
10
 
 
4
  from datetime import datetime
5
  from typing import Any, Dict, List
6
 
7
+ from .base import BaseTool, ToolResult
8
  from .registry import tool_registry
9
 
10