nc1709 1.15.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nc1709/__init__.py +13 -0
- nc1709/agent/__init__.py +36 -0
- nc1709/agent/core.py +505 -0
- nc1709/agent/mcp_bridge.py +245 -0
- nc1709/agent/permissions.py +298 -0
- nc1709/agent/tools/__init__.py +21 -0
- nc1709/agent/tools/base.py +440 -0
- nc1709/agent/tools/bash_tool.py +367 -0
- nc1709/agent/tools/file_tools.py +454 -0
- nc1709/agent/tools/notebook_tools.py +516 -0
- nc1709/agent/tools/search_tools.py +322 -0
- nc1709/agent/tools/task_tool.py +284 -0
- nc1709/agent/tools/web_tools.py +555 -0
- nc1709/agents/__init__.py +17 -0
- nc1709/agents/auto_fix.py +506 -0
- nc1709/agents/test_generator.py +507 -0
- nc1709/checkpoints.py +372 -0
- nc1709/cli.py +3380 -0
- nc1709/cli_ui.py +1080 -0
- nc1709/cognitive/__init__.py +149 -0
- nc1709/cognitive/anticipation.py +594 -0
- nc1709/cognitive/context_engine.py +1046 -0
- nc1709/cognitive/council.py +824 -0
- nc1709/cognitive/learning.py +761 -0
- nc1709/cognitive/router.py +583 -0
- nc1709/cognitive/system.py +519 -0
- nc1709/config.py +155 -0
- nc1709/custom_commands.py +300 -0
- nc1709/executor.py +333 -0
- nc1709/file_controller.py +354 -0
- nc1709/git_integration.py +308 -0
- nc1709/github_integration.py +477 -0
- nc1709/image_input.py +446 -0
- nc1709/linting.py +519 -0
- nc1709/llm_adapter.py +667 -0
- nc1709/logger.py +192 -0
- nc1709/mcp/__init__.py +18 -0
- nc1709/mcp/client.py +370 -0
- nc1709/mcp/manager.py +407 -0
- nc1709/mcp/protocol.py +210 -0
- nc1709/mcp/server.py +473 -0
- nc1709/memory/__init__.py +20 -0
- nc1709/memory/embeddings.py +325 -0
- nc1709/memory/indexer.py +474 -0
- nc1709/memory/sessions.py +432 -0
- nc1709/memory/vector_store.py +451 -0
- nc1709/models/__init__.py +86 -0
- nc1709/models/detector.py +377 -0
- nc1709/models/formats.py +315 -0
- nc1709/models/manager.py +438 -0
- nc1709/models/registry.py +497 -0
- nc1709/performance/__init__.py +343 -0
- nc1709/performance/cache.py +705 -0
- nc1709/performance/pipeline.py +611 -0
- nc1709/performance/tiering.py +543 -0
- nc1709/plan_mode.py +362 -0
- nc1709/plugins/__init__.py +17 -0
- nc1709/plugins/agents/__init__.py +18 -0
- nc1709/plugins/agents/django_agent.py +912 -0
- nc1709/plugins/agents/docker_agent.py +623 -0
- nc1709/plugins/agents/fastapi_agent.py +887 -0
- nc1709/plugins/agents/git_agent.py +731 -0
- nc1709/plugins/agents/nextjs_agent.py +867 -0
- nc1709/plugins/base.py +359 -0
- nc1709/plugins/manager.py +411 -0
- nc1709/plugins/registry.py +337 -0
- nc1709/progress.py +443 -0
- nc1709/prompts/__init__.py +22 -0
- nc1709/prompts/agent_system.py +180 -0
- nc1709/prompts/task_prompts.py +340 -0
- nc1709/prompts/unified_prompt.py +133 -0
- nc1709/reasoning_engine.py +541 -0
- nc1709/remote_client.py +266 -0
- nc1709/shell_completions.py +349 -0
- nc1709/slash_commands.py +649 -0
- nc1709/task_classifier.py +408 -0
- nc1709/version_check.py +177 -0
- nc1709/web/__init__.py +8 -0
- nc1709/web/server.py +950 -0
- nc1709/web/templates/index.html +1127 -0
- nc1709-1.15.4.dist-info/METADATA +858 -0
- nc1709-1.15.4.dist-info/RECORD +86 -0
- nc1709-1.15.4.dist-info/WHEEL +5 -0
- nc1709-1.15.4.dist-info/entry_points.txt +2 -0
- nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
- nc1709-1.15.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Search Tools
|
|
3
|
+
|
|
4
|
+
Tools for searching file contents:
|
|
5
|
+
- Grep: Search for patterns in files
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Optional, List
|
|
12
|
+
|
|
13
|
+
from .base import Tool, ToolResult, ToolParameter, ToolPermission
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class GrepTool(Tool):
|
|
17
|
+
"""Search for patterns in files"""
|
|
18
|
+
|
|
19
|
+
name = "Grep"
|
|
20
|
+
description = (
|
|
21
|
+
"Search for a pattern in files using regex. "
|
|
22
|
+
"Returns matching lines with file paths and line numbers. "
|
|
23
|
+
"Supports full regex syntax."
|
|
24
|
+
)
|
|
25
|
+
category = "search"
|
|
26
|
+
permission = ToolPermission.AUTO # Safe to auto-execute
|
|
27
|
+
|
|
28
|
+
parameters = [
|
|
29
|
+
ToolParameter(
|
|
30
|
+
name="pattern",
|
|
31
|
+
description="Regular expression pattern to search for",
|
|
32
|
+
type="string",
|
|
33
|
+
required=True,
|
|
34
|
+
),
|
|
35
|
+
ToolParameter(
|
|
36
|
+
name="path",
|
|
37
|
+
description="File or directory to search in (defaults to current directory)",
|
|
38
|
+
type="string",
|
|
39
|
+
required=False,
|
|
40
|
+
default=".",
|
|
41
|
+
),
|
|
42
|
+
ToolParameter(
|
|
43
|
+
name="glob",
|
|
44
|
+
description="Glob pattern to filter files (e.g., '*.py', '*.ts')",
|
|
45
|
+
type="string",
|
|
46
|
+
required=False,
|
|
47
|
+
),
|
|
48
|
+
ToolParameter(
|
|
49
|
+
name="case_insensitive",
|
|
50
|
+
description="Perform case-insensitive search",
|
|
51
|
+
type="boolean",
|
|
52
|
+
required=False,
|
|
53
|
+
default=False,
|
|
54
|
+
),
|
|
55
|
+
ToolParameter(
|
|
56
|
+
name="context_lines",
|
|
57
|
+
description="Number of context lines to show before and after matches",
|
|
58
|
+
type="integer",
|
|
59
|
+
required=False,
|
|
60
|
+
default=0,
|
|
61
|
+
),
|
|
62
|
+
ToolParameter(
|
|
63
|
+
name="max_results",
|
|
64
|
+
description="Maximum number of results to return",
|
|
65
|
+
type="integer",
|
|
66
|
+
required=False,
|
|
67
|
+
default=50,
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
def execute(
|
|
72
|
+
self,
|
|
73
|
+
pattern: str,
|
|
74
|
+
path: str = ".",
|
|
75
|
+
glob: str = None,
|
|
76
|
+
case_insensitive: bool = False,
|
|
77
|
+
context_lines: int = 0,
|
|
78
|
+
max_results: int = 50,
|
|
79
|
+
) -> ToolResult:
|
|
80
|
+
"""Search for pattern in files"""
|
|
81
|
+
|
|
82
|
+
base_path = Path(path).expanduser()
|
|
83
|
+
|
|
84
|
+
if not base_path.exists():
|
|
85
|
+
return ToolResult(
|
|
86
|
+
success=False,
|
|
87
|
+
output="",
|
|
88
|
+
error=f"Path not found: {path}",
|
|
89
|
+
target=pattern,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
# Compile regex
|
|
94
|
+
flags = re.IGNORECASE if case_insensitive else 0
|
|
95
|
+
try:
|
|
96
|
+
regex = re.compile(pattern, flags)
|
|
97
|
+
except re.error as e:
|
|
98
|
+
return ToolResult(
|
|
99
|
+
success=False,
|
|
100
|
+
output="",
|
|
101
|
+
error=f"Invalid regex pattern: {e}",
|
|
102
|
+
target=pattern,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# Find files to search
|
|
106
|
+
if base_path.is_file():
|
|
107
|
+
files = [base_path]
|
|
108
|
+
else:
|
|
109
|
+
if glob:
|
|
110
|
+
files = list(base_path.glob(f"**/{glob}"))
|
|
111
|
+
else:
|
|
112
|
+
# Search common code files by default
|
|
113
|
+
files = []
|
|
114
|
+
for ext in ["*.py", "*.js", "*.ts", "*.tsx", "*.jsx", "*.go", "*.rs",
|
|
115
|
+
"*.java", "*.c", "*.cpp", "*.h", "*.hpp", "*.md", "*.txt",
|
|
116
|
+
"*.json", "*.yaml", "*.yml", "*.toml", "*.sh", "*.bash"]:
|
|
117
|
+
files.extend(base_path.glob(f"**/{ext}"))
|
|
118
|
+
|
|
119
|
+
files = [f for f in files if f.is_file()]
|
|
120
|
+
|
|
121
|
+
# Skip binary files and very large files
|
|
122
|
+
max_file_size = 1_000_000 # 1MB
|
|
123
|
+
text_files = []
|
|
124
|
+
for f in files:
|
|
125
|
+
try:
|
|
126
|
+
if f.stat().st_size <= max_file_size:
|
|
127
|
+
text_files.append(f)
|
|
128
|
+
except OSError:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
# Search files
|
|
132
|
+
results = []
|
|
133
|
+
total_matches = 0
|
|
134
|
+
|
|
135
|
+
for file_path in text_files:
|
|
136
|
+
if total_matches >= max_results:
|
|
137
|
+
break
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
with open(file_path, "r", encoding="utf-8", errors="replace") as f:
|
|
141
|
+
lines = f.readlines()
|
|
142
|
+
|
|
143
|
+
for i, line in enumerate(lines):
|
|
144
|
+
if regex.search(line):
|
|
145
|
+
total_matches += 1
|
|
146
|
+
|
|
147
|
+
# Get context
|
|
148
|
+
start = max(0, i - context_lines)
|
|
149
|
+
end = min(len(lines), i + context_lines + 1)
|
|
150
|
+
|
|
151
|
+
context = []
|
|
152
|
+
for j in range(start, end):
|
|
153
|
+
prefix = ">" if j == i else " "
|
|
154
|
+
context.append(f"{prefix} {j+1:4}: {lines[j].rstrip()}")
|
|
155
|
+
|
|
156
|
+
rel_path = file_path.relative_to(base_path) if file_path.is_relative_to(base_path) else file_path
|
|
157
|
+
results.append({
|
|
158
|
+
"file": str(rel_path),
|
|
159
|
+
"line": i + 1,
|
|
160
|
+
"content": line.rstrip(),
|
|
161
|
+
"context": "\n".join(context) if context_lines > 0 else None,
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
if total_matches >= max_results:
|
|
165
|
+
break
|
|
166
|
+
|
|
167
|
+
except (OSError, UnicodeDecodeError):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
# Format output
|
|
171
|
+
if not results:
|
|
172
|
+
return ToolResult(
|
|
173
|
+
success=True,
|
|
174
|
+
output=f"No matches found for pattern: {pattern}",
|
|
175
|
+
target=pattern,
|
|
176
|
+
data={"count": 0, "matches": []},
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
output_lines = [f"Found {len(results)} match(es) for '{pattern}':"]
|
|
180
|
+
for r in results:
|
|
181
|
+
output_lines.append(f"\n{r['file']}:{r['line']}")
|
|
182
|
+
if r.get("context"):
|
|
183
|
+
output_lines.append(r["context"])
|
|
184
|
+
else:
|
|
185
|
+
output_lines.append(f" {r['content'][:200]}")
|
|
186
|
+
|
|
187
|
+
if total_matches >= max_results:
|
|
188
|
+
output_lines.append(f"\n... (limited to {max_results} results)")
|
|
189
|
+
|
|
190
|
+
return ToolResult(
|
|
191
|
+
success=True,
|
|
192
|
+
output="\n".join(output_lines),
|
|
193
|
+
target=pattern,
|
|
194
|
+
data={"count": len(results), "matches": results},
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return ToolResult(
|
|
199
|
+
success=False,
|
|
200
|
+
output="",
|
|
201
|
+
error=f"Error searching: {e}",
|
|
202
|
+
target=pattern,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class RipgrepTool(Tool):
|
|
207
|
+
"""Fast search using ripgrep (if available)"""
|
|
208
|
+
|
|
209
|
+
name = "Ripgrep"
|
|
210
|
+
description = (
|
|
211
|
+
"Fast file search using ripgrep (rg). "
|
|
212
|
+
"Falls back to Python grep if ripgrep is not installed."
|
|
213
|
+
)
|
|
214
|
+
category = "search"
|
|
215
|
+
permission = ToolPermission.AUTO
|
|
216
|
+
|
|
217
|
+
parameters = [
|
|
218
|
+
ToolParameter(
|
|
219
|
+
name="pattern",
|
|
220
|
+
description="Search pattern (regex)",
|
|
221
|
+
type="string",
|
|
222
|
+
required=True,
|
|
223
|
+
),
|
|
224
|
+
ToolParameter(
|
|
225
|
+
name="path",
|
|
226
|
+
description="Path to search in",
|
|
227
|
+
type="string",
|
|
228
|
+
required=False,
|
|
229
|
+
default=".",
|
|
230
|
+
),
|
|
231
|
+
ToolParameter(
|
|
232
|
+
name="type",
|
|
233
|
+
description="File type to search (e.g., 'py', 'js', 'ts')",
|
|
234
|
+
type="string",
|
|
235
|
+
required=False,
|
|
236
|
+
),
|
|
237
|
+
ToolParameter(
|
|
238
|
+
name="case_insensitive",
|
|
239
|
+
description="Case-insensitive search",
|
|
240
|
+
type="boolean",
|
|
241
|
+
required=False,
|
|
242
|
+
default=False,
|
|
243
|
+
),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
def execute(
|
|
247
|
+
self,
|
|
248
|
+
pattern: str,
|
|
249
|
+
path: str = ".",
|
|
250
|
+
type: str = None,
|
|
251
|
+
case_insensitive: bool = False,
|
|
252
|
+
) -> ToolResult:
|
|
253
|
+
"""Search using ripgrep"""
|
|
254
|
+
|
|
255
|
+
# Build command
|
|
256
|
+
cmd = ["rg", "--line-number", "--no-heading", "--color=never"]
|
|
257
|
+
|
|
258
|
+
if case_insensitive:
|
|
259
|
+
cmd.append("-i")
|
|
260
|
+
|
|
261
|
+
if type:
|
|
262
|
+
cmd.extend(["-t", type])
|
|
263
|
+
|
|
264
|
+
cmd.extend(["--max-count=100", pattern, path])
|
|
265
|
+
|
|
266
|
+
try:
|
|
267
|
+
result = subprocess.run(
|
|
268
|
+
cmd,
|
|
269
|
+
capture_output=True,
|
|
270
|
+
text=True,
|
|
271
|
+
timeout=30,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
if result.returncode == 0:
|
|
275
|
+
output = result.stdout
|
|
276
|
+
lines = output.strip().split("\n") if output.strip() else []
|
|
277
|
+
return ToolResult(
|
|
278
|
+
success=True,
|
|
279
|
+
output=output if output else "No matches found",
|
|
280
|
+
target=pattern,
|
|
281
|
+
data={"count": len(lines)},
|
|
282
|
+
)
|
|
283
|
+
elif result.returncode == 1:
|
|
284
|
+
# No matches
|
|
285
|
+
return ToolResult(
|
|
286
|
+
success=True,
|
|
287
|
+
output="No matches found",
|
|
288
|
+
target=pattern,
|
|
289
|
+
data={"count": 0},
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
return ToolResult(
|
|
293
|
+
success=False,
|
|
294
|
+
output="",
|
|
295
|
+
error=result.stderr or "ripgrep error",
|
|
296
|
+
target=pattern,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
except FileNotFoundError:
|
|
300
|
+
# ripgrep not installed, fall back to grep tool
|
|
301
|
+
grep = GrepTool()
|
|
302
|
+
return grep.execute(pattern=pattern, path=path, case_insensitive=case_insensitive)
|
|
303
|
+
except subprocess.TimeoutExpired:
|
|
304
|
+
return ToolResult(
|
|
305
|
+
success=False,
|
|
306
|
+
output="",
|
|
307
|
+
error="Search timed out",
|
|
308
|
+
target=pattern,
|
|
309
|
+
)
|
|
310
|
+
except Exception as e:
|
|
311
|
+
return ToolResult(
|
|
312
|
+
success=False,
|
|
313
|
+
output="",
|
|
314
|
+
error=f"Search error: {e}",
|
|
315
|
+
target=pattern,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def register_search_tools(registry):
|
|
320
|
+
"""Register all search tools with a registry"""
|
|
321
|
+
registry.register_class(GrepTool)
|
|
322
|
+
registry.register_class(RipgrepTool)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task Tool - Sub-agent Spawning
|
|
3
|
+
|
|
4
|
+
Allows spawning sub-agents for complex, multi-step tasks.
|
|
5
|
+
Each sub-agent can use tools and work autonomously.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
9
|
+
import threading
|
|
10
|
+
import queue
|
|
11
|
+
|
|
12
|
+
from .base import Tool, ToolResult, ToolParameter, ToolPermission
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from ..core import Agent
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TaskTool(Tool):
|
|
19
|
+
"""Spawn a sub-agent for complex tasks"""
|
|
20
|
+
|
|
21
|
+
name = "Task"
|
|
22
|
+
description = (
|
|
23
|
+
"Launch a sub-agent to handle a complex, multi-step task autonomously. "
|
|
24
|
+
"The sub-agent has access to all tools and can work independently. "
|
|
25
|
+
"Use for tasks that require multiple steps or exploration."
|
|
26
|
+
)
|
|
27
|
+
category = "agent"
|
|
28
|
+
permission = ToolPermission.ASK # Ask before spawning
|
|
29
|
+
|
|
30
|
+
parameters = [
|
|
31
|
+
ToolParameter(
|
|
32
|
+
name="prompt",
|
|
33
|
+
description="Detailed task description for the sub-agent",
|
|
34
|
+
type="string",
|
|
35
|
+
required=True,
|
|
36
|
+
),
|
|
37
|
+
ToolParameter(
|
|
38
|
+
name="description",
|
|
39
|
+
description="Short description of the task (3-5 words)",
|
|
40
|
+
type="string",
|
|
41
|
+
required=True,
|
|
42
|
+
),
|
|
43
|
+
ToolParameter(
|
|
44
|
+
name="agent_type",
|
|
45
|
+
description="Type of agent to spawn",
|
|
46
|
+
type="string",
|
|
47
|
+
required=False,
|
|
48
|
+
default="general",
|
|
49
|
+
enum=["general", "explore", "code", "research"],
|
|
50
|
+
),
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
def __init__(self, parent_agent: "Agent" = None):
|
|
54
|
+
super().__init__()
|
|
55
|
+
self.parent_agent = parent_agent
|
|
56
|
+
self._running_tasks: Dict[str, Dict] = {}
|
|
57
|
+
self._task_counter = 0
|
|
58
|
+
|
|
59
|
+
def set_parent_agent(self, agent: "Agent") -> None:
|
|
60
|
+
"""Set the parent agent for spawning sub-agents"""
|
|
61
|
+
self.parent_agent = agent
|
|
62
|
+
|
|
63
|
+
def execute(
|
|
64
|
+
self,
|
|
65
|
+
prompt: str,
|
|
66
|
+
description: str,
|
|
67
|
+
agent_type: str = "general",
|
|
68
|
+
) -> ToolResult:
|
|
69
|
+
"""Spawn a sub-agent to handle a task"""
|
|
70
|
+
|
|
71
|
+
if not self.parent_agent:
|
|
72
|
+
return ToolResult(
|
|
73
|
+
success=False,
|
|
74
|
+
output="",
|
|
75
|
+
error="No parent agent configured for task spawning",
|
|
76
|
+
target=description,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Create sub-agent with same configuration
|
|
81
|
+
from ..core import Agent, AgentConfig
|
|
82
|
+
|
|
83
|
+
# Configure sub-agent based on type
|
|
84
|
+
config = AgentConfig(
|
|
85
|
+
max_iterations=20, # Limit sub-agent iterations
|
|
86
|
+
tool_permissions=self.parent_agent.config.tool_permissions.copy(),
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Create sub-agent
|
|
90
|
+
sub_agent = Agent(
|
|
91
|
+
llm=self.parent_agent.llm,
|
|
92
|
+
config=config,
|
|
93
|
+
parent_agent=self.parent_agent,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Add context about this being a sub-agent
|
|
97
|
+
context = f"""You are a sub-agent spawned to handle a specific task.
|
|
98
|
+
Your task: {description}
|
|
99
|
+
|
|
100
|
+
Detailed instructions:
|
|
101
|
+
{prompt}
|
|
102
|
+
|
|
103
|
+
Work autonomously to complete this task. Use available tools as needed.
|
|
104
|
+
When complete, provide a clear summary of what you accomplished.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Run sub-agent synchronously for now
|
|
108
|
+
# (could be made async for parallel tasks)
|
|
109
|
+
result = sub_agent.run(context)
|
|
110
|
+
|
|
111
|
+
return ToolResult(
|
|
112
|
+
success=True,
|
|
113
|
+
output=f"Sub-agent completed task: {description}\n\nResult:\n{result}",
|
|
114
|
+
target=description,
|
|
115
|
+
data={
|
|
116
|
+
"agent_type": agent_type,
|
|
117
|
+
"iterations": sub_agent.iteration_count,
|
|
118
|
+
},
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
except Exception as e:
|
|
122
|
+
return ToolResult(
|
|
123
|
+
success=False,
|
|
124
|
+
output="",
|
|
125
|
+
error=f"Error running sub-agent: {e}",
|
|
126
|
+
target=description,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TodoWriteTool(Tool):
|
|
131
|
+
"""Manage a task list for tracking progress"""
|
|
132
|
+
|
|
133
|
+
name = "TodoWrite"
|
|
134
|
+
description = (
|
|
135
|
+
"Create or update a todo list to track task progress. "
|
|
136
|
+
"Use this to plan complex tasks and show progress to the user."
|
|
137
|
+
)
|
|
138
|
+
category = "agent"
|
|
139
|
+
permission = ToolPermission.AUTO # Safe to auto-execute
|
|
140
|
+
|
|
141
|
+
parameters = [
|
|
142
|
+
ToolParameter(
|
|
143
|
+
name="todos",
|
|
144
|
+
description="List of todo items with status",
|
|
145
|
+
type="array",
|
|
146
|
+
required=True,
|
|
147
|
+
),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
# Class-level todo storage
|
|
151
|
+
_todos: list = []
|
|
152
|
+
|
|
153
|
+
def execute(self, todos: list) -> ToolResult:
|
|
154
|
+
"""Update the todo list"""
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
# Validate todo format
|
|
158
|
+
validated_todos = []
|
|
159
|
+
for item in todos:
|
|
160
|
+
if isinstance(item, dict):
|
|
161
|
+
validated_todos.append({
|
|
162
|
+
"content": item.get("content", ""),
|
|
163
|
+
"status": item.get("status", "pending"),
|
|
164
|
+
"activeForm": item.get("activeForm", item.get("content", "")),
|
|
165
|
+
})
|
|
166
|
+
elif isinstance(item, str):
|
|
167
|
+
validated_todos.append({
|
|
168
|
+
"content": item,
|
|
169
|
+
"status": "pending",
|
|
170
|
+
"activeForm": item,
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
TodoWriteTool._todos = validated_todos
|
|
174
|
+
|
|
175
|
+
# Format output
|
|
176
|
+
output_lines = ["Todo list updated:"]
|
|
177
|
+
for i, todo in enumerate(validated_todos, 1):
|
|
178
|
+
status_icon = {
|
|
179
|
+
"pending": "○",
|
|
180
|
+
"in_progress": "●",
|
|
181
|
+
"completed": "✓",
|
|
182
|
+
}.get(todo["status"], "○")
|
|
183
|
+
output_lines.append(f" {status_icon} {i}. {todo['content']}")
|
|
184
|
+
|
|
185
|
+
return ToolResult(
|
|
186
|
+
success=True,
|
|
187
|
+
output="\n".join(output_lines),
|
|
188
|
+
target=f"{len(validated_todos)} items",
|
|
189
|
+
data={"todos": validated_todos},
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
return ToolResult(
|
|
194
|
+
success=False,
|
|
195
|
+
output="",
|
|
196
|
+
error=f"Error updating todos: {e}",
|
|
197
|
+
target="todos",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def get_todos(cls) -> list:
|
|
202
|
+
"""Get current todo list"""
|
|
203
|
+
return cls._todos.copy()
|
|
204
|
+
|
|
205
|
+
@classmethod
|
|
206
|
+
def clear_todos(cls) -> None:
|
|
207
|
+
"""Clear the todo list"""
|
|
208
|
+
cls._todos = []
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class AskUserTool(Tool):
|
|
212
|
+
"""Ask the user a question"""
|
|
213
|
+
|
|
214
|
+
name = "AskUser"
|
|
215
|
+
description = (
|
|
216
|
+
"Ask the user a question to clarify requirements or get a decision. "
|
|
217
|
+
"Use when you need user input to proceed."
|
|
218
|
+
)
|
|
219
|
+
category = "agent"
|
|
220
|
+
permission = ToolPermission.AUTO # Safe - just asks question
|
|
221
|
+
|
|
222
|
+
parameters = [
|
|
223
|
+
ToolParameter(
|
|
224
|
+
name="question",
|
|
225
|
+
description="The question to ask the user",
|
|
226
|
+
type="string",
|
|
227
|
+
required=True,
|
|
228
|
+
),
|
|
229
|
+
ToolParameter(
|
|
230
|
+
name="options",
|
|
231
|
+
description="Optional list of choices for the user",
|
|
232
|
+
type="array",
|
|
233
|
+
required=False,
|
|
234
|
+
),
|
|
235
|
+
]
|
|
236
|
+
|
|
237
|
+
def __init__(self, input_callback=None):
|
|
238
|
+
super().__init__()
|
|
239
|
+
self.input_callback = input_callback or input
|
|
240
|
+
|
|
241
|
+
def execute(self, question: str, options: list = None) -> ToolResult:
|
|
242
|
+
"""Ask user a question"""
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Display question
|
|
246
|
+
print(f"\n❓ {question}")
|
|
247
|
+
|
|
248
|
+
if options:
|
|
249
|
+
for i, opt in enumerate(options, 1):
|
|
250
|
+
print(f" {i}. {opt}")
|
|
251
|
+
print()
|
|
252
|
+
|
|
253
|
+
# Get user input
|
|
254
|
+
response = self.input_callback("> ").strip()
|
|
255
|
+
|
|
256
|
+
# If options provided and user entered a number, map to option
|
|
257
|
+
if options and response.isdigit():
|
|
258
|
+
idx = int(response) - 1
|
|
259
|
+
if 0 <= idx < len(options):
|
|
260
|
+
response = options[idx]
|
|
261
|
+
|
|
262
|
+
return ToolResult(
|
|
263
|
+
success=True,
|
|
264
|
+
output=f"User response: {response}",
|
|
265
|
+
target=question[:30],
|
|
266
|
+
data={"response": response},
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
return ToolResult(
|
|
271
|
+
success=False,
|
|
272
|
+
output="",
|
|
273
|
+
error=f"Error getting user input: {e}",
|
|
274
|
+
target=question[:30],
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def register_task_tools(registry, parent_agent=None):
|
|
279
|
+
"""Register task management tools with a registry"""
|
|
280
|
+
task_tool = TaskTool(parent_agent)
|
|
281
|
+
registry.register(task_tool)
|
|
282
|
+
registry.register_class(TodoWriteTool)
|
|
283
|
+
registry.register_class(AskUserTool)
|
|
284
|
+
return task_tool # Return so parent_agent can be set later
|