vamp-cli 0.1.0__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.
Files changed (57) hide show
  1. vamp_cli/__init__.py +0 -0
  2. vamp_cli/agent/__init__.py +0 -0
  3. vamp_cli/agent/context_sync.py +72 -0
  4. vamp_cli/agent/graph.py +159 -0
  5. vamp_cli/agent/instructions.py +18 -0
  6. vamp_cli/agent/nodes.py +375 -0
  7. vamp_cli/agent/roles.py +8 -0
  8. vamp_cli/agent/runtime/__init__.py +0 -0
  9. vamp_cli/agent/runtime/runtime.py +133 -0
  10. vamp_cli/agent/state.py +55 -0
  11. vamp_cli/agent/status.py +17 -0
  12. vamp_cli/config/__init__.py +0 -0
  13. vamp_cli/config/loader.py +152 -0
  14. vamp_cli/config/schema.py +82 -0
  15. vamp_cli/llm/__init__.py +0 -0
  16. vamp_cli/llm/adapter.py +52 -0
  17. vamp_cli/llm/adapter_registry.py +26 -0
  18. vamp_cli/llm/debug_callbacks.py +18 -0
  19. vamp_cli/llm/router.py +247 -0
  20. vamp_cli/llm/usage.py +170 -0
  21. vamp_cli/logging_setup/logging_setup.py +37 -0
  22. vamp_cli/main.py +1023 -0
  23. vamp_cli/persistence/__init__.py +29 -0
  24. vamp_cli/persistence/compaction.py +372 -0
  25. vamp_cli/persistence/emitter.py +371 -0
  26. vamp_cli/persistence/event_log.py +303 -0
  27. vamp_cli/persistence/fork.py +81 -0
  28. vamp_cli/persistence/loader.py +616 -0
  29. vamp_cli/persistence/transcript.py +160 -0
  30. vamp_cli/prompts/prompts.py +182 -0
  31. vamp_cli/sandbox/__init__.py +0 -0
  32. vamp_cli/sandbox/base.py +22 -0
  33. vamp_cli/sandbox/config.py +16 -0
  34. vamp_cli/sandbox/docker.py +293 -0
  35. vamp_cli/sandbox/manager.py +31 -0
  36. vamp_cli/sandbox/result.py +14 -0
  37. vamp_cli/sandbox/terminal.py +151 -0
  38. vamp_cli/tools/__init__.py +0 -0
  39. vamp_cli/tools/base.py +44 -0
  40. vamp_cli/tools/diagnostic.py +120 -0
  41. vamp_cli/tools/execute_cmd.py +16 -0
  42. vamp_cli/tools/files.py +652 -0
  43. vamp_cli/tools/registry.py +163 -0
  44. vamp_cli/tools/tools_classes.py +622 -0
  45. vamp_cli/utils/__init__.py +0 -0
  46. vamp_cli/utils/error_codes.py +7 -0
  47. vamp_cli/utils/ignore.py +36 -0
  48. vamp_cli/utils/sha_256.py +32 -0
  49. vamp_cli/utils/token_tracker.py +31 -0
  50. vamp_cli/workspace/__init__.py +0 -0
  51. vamp_cli/workspace/workspace.py +16 -0
  52. vamp_cli-0.1.0.dist-info/METADATA +317 -0
  53. vamp_cli-0.1.0.dist-info/RECORD +57 -0
  54. vamp_cli-0.1.0.dist-info/WHEEL +5 -0
  55. vamp_cli-0.1.0.dist-info/entry_points.txt +2 -0
  56. vamp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
  57. vamp_cli-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,622 @@
1
+ # tool_defs/file_tools_defs.py
2
+ import json
3
+ from typing import override
4
+ from vamp_cli.tools.base import Tools, InputSchema
5
+ from vamp_cli.tools.files import FileTools
6
+ from vamp_cli.tools.execute_cmd import ExecuteCommandTool
7
+
8
+
9
+ class ListDirTool(Tools):
10
+ name = "list_dir"
11
+ parallel_safe = True
12
+ description = (
13
+ "List the files and directories contained within a workspace directory."
14
+ "Use this tool when you need to inspect directory structure or discover what exists at a path."
15
+ "NOTE: BY DEFAULT ITS MAX DEPTH IS 2(for safetly in very large directories). To recurse more use specific path and max depth."
16
+ "It does not read file contents and does not search inside files."
17
+ "The listing recursively traverses subdirectories unless max_depth is specified."
18
+ ".gitignore rules are respected, including .gitignore files found inside nested directories. Symbolic links are skipped."
19
+ "The returned entries contain the filesystem path and whether each entry is a 'file' or 'directory'."
20
+ "Paths in the returned list are workspace-resolved paths."
21
+
22
+ "Use this instead of:"
23
+ "- 'file_search' when you want to see the directory structure, not find files by name."
24
+ "- 'read_file' when you do not yet need file contents."
25
+ "- 'ripgrep_search' when you are not searching file contents."
26
+ )
27
+ input_schema = InputSchema(
28
+ type="object",
29
+ properties={
30
+ "path": {
31
+ "type": "string",
32
+ "description": "Directory to inspect. Required."
33
+ },
34
+ "max_depth": {
35
+ "type": "integer",
36
+ "description": "Maximum traversal depth relative to path. 0 means do not traverse into the directory; 1 includes its immediate children; omit this parameter to traverse without a depth limit.",
37
+ },
38
+ },
39
+ required=["path"],
40
+ additionalProperties=False,
41
+ )
42
+
43
+ def __init__(self, file_tools: FileTools):
44
+ self.file_tools = file_tools
45
+
46
+ @override
47
+ def run(self, **inputs) -> str:
48
+ path = inputs["path"]
49
+ max_depth = inputs.get("max_depth", 2)
50
+ result = self.file_tools.list_dir(path, max_depth)
51
+ return json.dumps(result)
52
+
53
+
54
+ class ReadFileTool(Tools):
55
+ name = "read_file"
56
+ parallel_safe = True
57
+ description = (
58
+ "Read the text content of a specific file."
59
+ "Use this tool when you already know the file path and need to inspect its contents. It supports reading either the beginning of the file or a specific inclusive line range."
60
+
61
+ "Line numbers are 1-indexed. A single call can return at most 500 lines. If the requested range is larger, it is automatically capped to 500 lines."
62
+
63
+ "The returned content includes the source line number before each line."
64
+ "The response also reports the actual returned range, total number of lines in the file, and file size in bytes."
65
+
66
+ "The file is decoded as UTF-8; invalid UTF-8 bytes are replaced rather than causing decoding to fail."
67
+
68
+ "Use this instead of:"
69
+ "- 'list_dir' when you need file contents."
70
+ "- 'ripgrep_search' when you already know the file and want to inspect surrounding code or a contiguous section."
71
+ "- 'file_search' when you are looking for text inside a file rather than in its filename."
72
+ )
73
+
74
+ input_schema = InputSchema(
75
+ type="object",
76
+ properties={
77
+ "path": {
78
+ "type": "string",
79
+ "description": "Path of the file to read. Required."
80
+ },
81
+ "start": {
82
+ "type": "integer",
83
+ "description": "First line to return, 1-indexed. Defaults to line 1."
84
+ },
85
+ "end": {
86
+ "type": "integer",
87
+ "description": "Last line to return, inclusive. Defaults to start + 499."
88
+ },
89
+ },
90
+ required=["path"],
91
+ additionalProperties=False,
92
+ )
93
+
94
+ def __init__(self, file_tools: FileTools):
95
+ self.file_tools = file_tools
96
+
97
+ @override
98
+ def run(self, **inputs) -> str:
99
+ path = inputs["path"]
100
+ start = inputs.get("start")
101
+ end = inputs.get("end")
102
+ result = self.file_tools.read_file(path, start, end)
103
+ return json.dumps(result)
104
+
105
+
106
+ class ReadManyFilesTool(Tools):
107
+ name = "read_many_files"
108
+ parallel_safe = True
109
+ description = (
110
+ "Read multiple files in a single call."
111
+ "Use this tool when you need to inspect the contents of several files at once."
112
+ "It accepts a list of file paths and optional line range parameters."
113
+ "For each file path, validates it's within workspace scope."
114
+ "Returns a structured result containing the content of all requested files"
115
+ "with their paths, line ranges, and any errors for files that couldn't be read."
116
+
117
+ "Line numbers are 1-indexed. A single call can return at most 500 lines per file."
118
+ "If the requested range is larger, it is automatically capped to 500 lines."
119
+
120
+ "The returned content includes the source line number before each line."
121
+ "The response also reports the actual returned range, total number of lines in the file, and file size in bytes."
122
+
123
+ "The file is decoded as UTF-8; invalid UTF-8 bytes are replaced rather than causing decoding to fail."
124
+
125
+ "Use this instead of:"
126
+ "- Multiple 'read_file' calls when you need to read several files."
127
+ "- 'file_search' when you already know the file paths and need file contents."
128
+ )
129
+
130
+ input_schema = InputSchema(
131
+ type="object",
132
+ properties={
133
+ "paths": {
134
+ "type": "array",
135
+ "items": {
136
+ "type": "string"
137
+ },
138
+ "description": "List of file paths to read. Required."
139
+ },
140
+ "start": {
141
+ "type": "integer",
142
+ "description": "First line to return for all files, defaults to 1."
143
+ },
144
+ "end": {
145
+ "type": "integer",
146
+ "description": "Last line to return for all files, defaults to start + 499."
147
+ },
148
+ },
149
+ required=["paths"],
150
+ additionalProperties=False,
151
+ )
152
+
153
+ def __init__(self, file_tools: FileTools):
154
+ self.file_tools = file_tools
155
+
156
+ @override
157
+ def run(self, **inputs) -> str:
158
+ paths = inputs["paths"]
159
+ start = inputs.get("start")
160
+ end = inputs.get("end")
161
+ result = self.file_tools.read_many_files(paths, start, end)
162
+ return json.dumps(result)
163
+
164
+
165
+ class WriteFileTool(Tools):
166
+ name = "write_file"
167
+ parallel_safe = False
168
+ description = (
169
+ "Create a file or replace the complete contents of an existing file with the supplied content."
170
+
171
+ "Use this tool when you need to create a new file or intentionally rewrite an entire file."
172
+
173
+ "By default, an existing file is protected and will not be modified. Set overwrite=true only when the entire existing file should be replaced."
174
+
175
+ "Parent directories are created automatically when necessary."
176
+
177
+ "After writing, the tool performs a syntax check and may return a syntax_warning when the modification was successfully written but the resulting file contains a detected syntax error."
178
+
179
+ "This tool is for full-file replacement, not for making a small targeted edit inside an existing file."
180
+
181
+ "Use this instead of:"
182
+ "- 'apply_search_replace' when you intend to replace the entire file."
183
+ "- 'apply_search_replace' when the desired change is too broad or when you already have the complete desired file contents."
184
+
185
+ "Do not use this for"
186
+ "A small modification to an existing file when preserving the rest of the file is important. Use apply_search_replace instead."
187
+ )
188
+
189
+ input_schema = InputSchema(
190
+ type="object",
191
+ properties={
192
+ "path": {
193
+ "type": "string",
194
+ "description": "Destination file path. Required."
195
+ },
196
+ "content": {
197
+ "type": "string",
198
+ "description": "Complete new contents of the file. Required."
199
+ },
200
+ "overwrite": {
201
+ "type": "boolean",
202
+ "description": "Whether to replace an existing file completely. Defaults to false.",
203
+ },
204
+ },
205
+ required=["path", "content"],
206
+ additionalProperties=False,
207
+ )
208
+
209
+ def __init__(self, file_tools: FileTools):
210
+ self.file_tools = file_tools
211
+
212
+ @override
213
+ def run(self, **inputs) -> str:
214
+ path = inputs["path"]
215
+ content = inputs["content"]
216
+ overwrite = inputs.get("overwrite", False)
217
+ result = self.file_tools.write_file(path, content, overwrite)
218
+ return json.dumps(result)
219
+
220
+
221
+ class ApplySearchReplaceTool(Tools):
222
+ name = "apply_search_replace"
223
+ parallel_safe = False
224
+ description = (
225
+ "Make a targeted edit to an existing file by replacing exactly one occurrence of an existing text block with a replacement text block."
226
+
227
+ "Use this tool when you need to modify only a specific section of an existing file while preserving all unrelated content."
228
+
229
+ "The tool searches for the search text in two stages:"
230
+
231
+ "1. It first looks for an exact character-for-character match."
232
+ "2. If no exact match exists, it tries a whitespace-tolerant fallback where runs of whitespace are treated as equivalent."
233
+
234
+ "The search block must identify exactly one location. If it matches multiple locations, the edit is not performed because the tool cannot safely determine which occurrence should be changed."
235
+
236
+ "The replacement affects only the matched block; the rest of the file remains unchanged."
237
+
238
+ "After the edit, the tool performs a syntax check and may return a syntax_warning if the resulting file contains a detected syntax error."
239
+
240
+ "Use this instead of:"
241
+ "- 'write_file' when making a localized edit to an existing file."
242
+ "- 'read_file' when the goal is modification rather than inspection."
243
+
244
+ "Important selection rule:"
245
+ "Provide enough surrounding code in search to make the target unique. Do not use a tiny fragment such as a common variable name or generic statement when the same text may occur multiple times."
246
+ )
247
+
248
+ input_schema = InputSchema(
249
+ type="object",
250
+ properties={
251
+ "file_path": {
252
+ "type": "string",
253
+ "description": "Path of the existing file to modify. Required."
254
+ },
255
+ "search": {
256
+ "type": "string",
257
+ "description": "Existing text block that uniquely identifies the section to replace. Required."
258
+ },
259
+ "replace": {
260
+ "type": "string",
261
+ "description": "New text that should replace the matched block. Required."
262
+ },
263
+ },
264
+ required=["file_path", "search", "replace"],
265
+ additionalProperties=False,
266
+ )
267
+
268
+ def __init__(self, file_tools: FileTools):
269
+ self.file_tools = file_tools
270
+
271
+ @override
272
+ def run(self, **inputs) -> str:
273
+ file_path = inputs["file_path"]
274
+ search = inputs["search"]
275
+ replace = inputs["replace"]
276
+ result = self.file_tools.apply_search_replace(file_path, search, replace)
277
+ return json.dumps(result)
278
+
279
+
280
+ class FileSearchTool(Tools):
281
+ name = "file_search"
282
+ parallel_safe = True
283
+ description = (
284
+ "Find files by their filename or path name, without searching their contents."
285
+
286
+ "Use this tool when you know part of a filename, a filename pattern, or a glob pattern and need to discover matching files."
287
+
288
+ "If query does not contain glob metacharacters, it is treated as a filename substring search by automatically surrounding it with *. For example, query='config' behaves like *config*."
289
+
290
+ "If query contains glob syntax such as *, ?, or character classes such as [abc], the query is used as the glob pattern directly."
291
+
292
+ "The search is performed with ripgrep's file listing functionality and respects ignore rules such as .gitignore."
293
+
294
+ "Results are returned as paths relative to the workspace root when possible."
295
+
296
+ "Examples:"
297
+ "query='config' → filenames containing config"
298
+ "query='*.py' → Python files"
299
+ "query='*test*' → filenames containing test"
300
+ "query='src/**/config.*' → matching config files under src"
301
+
302
+ "Use this instead of:"
303
+ "- 'ripgrep_search' when searching filenames."
304
+ "- 'list_dir' when you want matching files rather than a directory listing."
305
+
306
+ "Do not use this for:"
307
+ "Searching for a string inside file contents."
308
+ )
309
+
310
+ input_schema = InputSchema(
311
+ type="object",
312
+ properties={
313
+ "query": {
314
+ "type": "string",
315
+ "description": "Filename substring or glob pattern to search for. Required.",
316
+ },
317
+ "path": {
318
+ "type": "string",
319
+ "description": "Filename substring or glob pattern to search for. Required.",
320
+ },
321
+ "max_results": {
322
+ "type": "integer",
323
+ "description": "Maximum number of matching paths to return. Defaults to 100"},
324
+ },
325
+ required=["query", "path"],
326
+ additionalProperties=False,
327
+ )
328
+
329
+ def __init__(self, file_tools: FileTools):
330
+ self.file_tools = file_tools
331
+
332
+ @override
333
+ def run(self, **inputs) -> str:
334
+ query = inputs["query"]
335
+ path = inputs.get("path", ".")
336
+ max_results = inputs.get("max_results", 100)
337
+ result = self.file_tools.file_search(query, path, max_results)
338
+ return json.dumps(result)
339
+
340
+
341
+ class RipgrepSearchTool(Tools):
342
+ name = "ripgrep_search"
343
+ parallel_safe = True
344
+ description = (
345
+ "Search the contents of one or more files for a text string or regular expression and return the matching lines with their file paths and line numbers."
346
+
347
+ "Use this tool when you need to find specific text, symbols, strings, code patterns, or regex matches inside files."
348
+
349
+ "By default, is_regex=false, so query is treated as a literal string and the search is case-insensitive."
350
+
351
+ "When is_regex=true, query is interpreted as a regular expression. In this mode the regex search is case-sensitive unless the regular expression itself specifies case-insensitive behavior."
352
+
353
+ "The search can target either a single file or a directory."
354
+
355
+ "file_pattern optionally restricts which files are searched using a glob pattern such as *.py."
356
+
357
+ "Each returned match includes:"
358
+ "- the file path,"
359
+ "- the 1-indexed line number,"
360
+ "- the matching line content."
361
+
362
+ "Returned matching-line content is limited to 250 characters."
363
+
364
+ "Results are capped by max_matches; when the cap is reached, the result can be marked as truncated."
365
+
366
+ "Examples:"
367
+ "- Find a function name: query='execute_command'"
368
+ "- Find exact text ignoring case: query='TODO'"
369
+ "- Find a regex pattern: query='def\\s+\\w+\\(', is_regex=true"
370
+ "- Search only Python files: file_pattern='*.py'"
371
+
372
+ "Use this instead of:"
373
+ "- 'file_search' when searching inside file contents."
374
+ "- 'read_file' when you do not know where the relevant text is and need to locate it first."
375
+ "- 'apply_search_replace' when you only need to find, not modify, text."
376
+ )
377
+ input_schema = InputSchema(
378
+ type="object",
379
+ properties={
380
+ "query": {
381
+ "type": "string",
382
+ "description": "Literal text or regular expression to search for. Required."
383
+ },
384
+ "path": {
385
+ "type": "string",
386
+ "description": "File or directory to search. Defaults to '.'",
387
+ },
388
+ "is_regex": {
389
+ "type": "boolean",
390
+ "description": "Interpret query as a regular expression. Defaults to false. Required.",
391
+ },
392
+ "file_pattern": {
393
+ "type": "string",
394
+ "description": "Optional glob restricting which files are searched, for example *.py.",
395
+ },
396
+ "max_matches": {
397
+ "type": "integer",
398
+ "description": "Maximum number of matching lines to return. Defaults to 50.",
399
+ },
400
+ },
401
+ required=["query", "is_regex"],
402
+ additionalProperties=False,
403
+ )
404
+
405
+ def __init__(self, file_tools: FileTools):
406
+ self.file_tools = file_tools
407
+
408
+ @override
409
+ def run(self, **inputs) -> str:
410
+ query = inputs["query"]
411
+ path = inputs.get("path", ".")
412
+ is_regex = inputs.get("is_regex", False)
413
+ file_pattern = inputs.get("file_pattern")
414
+ max_matches = inputs.get("max_matches", 50)
415
+ result = self.file_tools.ripgrep_search(
416
+ query, path, is_regex, file_pattern, max_matches
417
+ )
418
+ return json.dumps(result)
419
+
420
+
421
+ class ExecuteCommandToolDef(Tools):
422
+ name = "execute_command"
423
+ parallel_safe = False
424
+ description = (
425
+ "Execute a shell command in the sandboxed terminal and return the command's execution result."
426
+
427
+ "Use this tool when the requested operation is best performed by an operating-system shell command or CLI program, such as running a build, test suite, formatter, package command, version-control command, script, compiler, or other executable."
428
+
429
+ "The command is executed with cwd as its working directory."
430
+
431
+ "This is a general-purpose command-execution tool and can perform actions beyond reading files, including commands that modify files, create processes, run programs, or otherwise change the workspace."
432
+
433
+ "Use this instead of:"
434
+ "- 'read_file' when the desired operation requires executing a command."
435
+ "- 'write_file' when a CLI program should perform the modification rather than directly writing content."
436
+ "- 'ripgrep_search' when you need functionality that requires an external shell command rather than text search."
437
+
438
+ "Important selection rule:"
439
+ "Do not use this tool merely to read, search, or edit a file when a dedicated file tool already provides the required operation more directly. Use shell execution when the task actually requires command execution or a CLI utility."
440
+ )
441
+ input_schema = InputSchema(
442
+ type="object",
443
+ properties={
444
+ "command": {
445
+ "type": "string",
446
+ "description": "Shell command to execute. Required."},
447
+ "cwd": {
448
+ "type": "string",
449
+ "description": "Working directory in which the command runs. Defaults to '.'",
450
+ },
451
+ },
452
+ required=["command"],
453
+ additionalProperties=False,
454
+ )
455
+
456
+ def __init__(self, execute_command_tool: ExecuteCommandTool):
457
+ self.execute_command_tool = execute_command_tool
458
+
459
+ @override
460
+ async def arun(self, **inputs) -> str:
461
+ command = inputs["command"]
462
+ cwd = inputs.get("cwd", ".")
463
+ result = await self.execute_command_tool.execute_command(command, cwd)
464
+ return json.dumps(result)
465
+
466
+ @override
467
+ def run(self, **inputs) -> str:
468
+ raise RuntimeError(
469
+ "execute_command must be invoked via arun(); sync run() is not supported."
470
+ )
471
+
472
+
473
+ class SubmitPlanTool(Tools):
474
+ name = "submit_plan"
475
+ parallel_safe = True
476
+ description = (
477
+ "Submit the final plan or answer and declare whether execution is required. "
478
+ "Call this exactly once when you are completely finished investigating and ready to hand off. "
479
+ "This tool does not read files, run commands, or modify anything. "
480
+ "It is a control signal used by the runtime to decide whether to end the run or hand the plan to the coder. "
481
+ "Use it when:"
482
+ "- You have finished all your tool calls and are ready to produce the final output."
483
+ "- You need the runtime to know whether the request is purely informational or requires execution."
484
+ "Do not use it before you are done, and do not call it more than once. "
485
+ "Set requires_execution to false only if the request is fully answered by the plan text alone and no files need to change and no commands need to run. "
486
+ "Otherwise set requires_execution to true."
487
+ )
488
+ input_schema = InputSchema(
489
+ type="object",
490
+ properties={
491
+ "plan": {
492
+ "type": "string",
493
+ "description": (
494
+ "The complete final plan or answer. "
495
+ "For execution requests, this should be a step-by-step plan the coder can follow. "
496
+ "For informational requests, this should be the full explanation or answer. Required."
497
+ ),
498
+ },
499
+ "requires_execution": {
500
+ "type": "boolean",
501
+ "description": (
502
+ "True if the request requires modifying files or running commands. "
503
+ "False if the request is purely informational and is fully answered by the 'plan' text."
504
+ "Required."
505
+ ),
506
+ },
507
+ },
508
+ required=["plan", "requires_execution"],
509
+ additionalProperties=False,
510
+ )
511
+
512
+ @override
513
+ def run(self, **inputs) -> str:
514
+ # This tool is a control signal and must be intercepted by the tool node.
515
+ # If run() is ever reached, the interception logic is missing.
516
+ # raise RuntimeError(
517
+ # "submit_plan is a control tool and must be intercepted before execution."
518
+ # )
519
+ return json.dumps({"status": "Plan submitted."})
520
+
521
+
522
+ class WriteTodosTool(Tools):
523
+ name = "write_update_todos"
524
+ parallel_safe = True
525
+ description = (
526
+ "Create or update the todo list that tracks the work you are performing for the current task."
527
+
528
+ "Use this tool to plan multi-step work before you begin, and to keep the list current as you make progress."
529
+
530
+ "The todo list is a structured checklist of discrete, verifiable tasks. Each item has a stable id, a short imperative description, and a status."
531
+
532
+ "This tool REPLACES the entire previous todo list with the supplied list on every call. Always send the COMPLETE list, not just the items that changed. If you have three items and one becomes completed, call this tool again with all three items, one of them marked completed."
533
+
534
+ "Exactly one item may be in_progress at a time. If you have no active work (for example between steps), all items may be pending. Never mark an item completed until the change has been applied and verified — if verification failed, mark it blocked instead."
535
+
536
+ "Keep item ids stable across updates so progress can be tracked. Do not renumber or reassign ids between calls."
537
+
538
+ "Call this tool:"
539
+ "- Once before you begin making changes, to lay out the plan as a todo list."
540
+ "- Again every time the status of any item changes."
541
+ "- Again when you discover additional work and need to add items."
542
+
543
+ "Use this instead of:"
544
+ "- Explaining your plan in prose when you intend to execute it. The todo list is the tracking mechanism the user sees."
545
+ "- Calling this tool with a partial list. It replaces the whole list; partial updates will lose items."
546
+
547
+ "Do not use this for:"
548
+ "- Purely informational answers that require no execution. In that case no todo list is needed."
549
+ "- Tracking sub-steps so small that no useful progress information is conveyed."
550
+ "NOTE:"
551
+ "- Always send the COMPLETE list, not just the items that changed."
552
+ )
553
+ input_schema = InputSchema(
554
+ type="object",
555
+ properties={
556
+ "todos": {
557
+ "type": "array",
558
+ "description": (
559
+ "The COMPLETE todo list, replacing any previous list. "
560
+ "Every item currently being tracked must be included. Required."
561
+ ),
562
+ "items": {
563
+ "type": "object",
564
+ "properties": {
565
+ "id": {
566
+ "type": "string",
567
+ "description": (
568
+ "Stable identifier for this item. Must not change across updates. Required."
569
+ ),
570
+ },
571
+ "description": {
572
+ "type": "string",
573
+ "description": (
574
+ "Short imperative description of the task, for example 'Add retry logic to client.py'. Required."
575
+ ),
576
+ },
577
+ "status": {
578
+ "type": "string",
579
+ "enum": [
580
+ "pending",
581
+ "in_progress",
582
+ "completed",
583
+ "blocked",
584
+ ],
585
+ "description": (
586
+ "Current status of this item. "
587
+ "'pending' means not started, "
588
+ "'in_progress' means currently being worked on, "
589
+ "'completed' means finished and verified, "
590
+ "'blocked' means it cannot proceed. Required."
591
+ ),
592
+ },
593
+ },
594
+ "required": ["id", "description", "status"],
595
+ "additionalProperties": False,
596
+ },
597
+ },
598
+ },
599
+ required=["todos"],
600
+ additionalProperties=False,
601
+ )
602
+
603
+ @override
604
+ def run(self, **inputs) -> str:
605
+ todos = inputs["todos"]
606
+
607
+ in_progress = [t for t in todos if t["status"] == "in_progress"]
608
+ if len(in_progress) > 1:
609
+ raise ValueError(
610
+ "Only one todo item may be 'in_progress' at a time. "
611
+ f"Found {len(in_progress)}: "
612
+ + ", ".join(t["id"] for t in in_progress)
613
+ )
614
+
615
+ seen_ids = set()
616
+ for t in todos:
617
+ if t["id"] in seen_ids:
618
+ raise ValueError(f"Duplicate todo id: {t['id']!r}")
619
+ seen_ids.add(t["id"])
620
+
621
+ # The tool node intercepts this payload and writes it into state["todo"].
622
+ return json.dumps(todos)
File without changes
@@ -0,0 +1,7 @@
1
+ from enum import Enum
2
+
3
+ class Error_codes(Enum):
4
+ PATH_SCOPE_ERROR = "Path is out of workspace"
5
+ NOT_FILE_ERROR = "Path is not a file path"
6
+ FILE_NOT_FOUND_ERROR = "File not found"
7
+
@@ -0,0 +1,36 @@
1
+ import pathspec
2
+ from pathlib import Path
3
+
4
+
5
+ class IgnoreMatcher:
6
+ def __init__(self, repo_root: str) -> None:
7
+ self.repo_root = Path(repo_root).resolve()
8
+ self.patterns: list[str] = []
9
+ self.spec = pathspec.PathSpec.from_lines("gitignore", self.patterns)
10
+
11
+ def _rebuild(self):
12
+ self.spec = pathspec.PathSpec.from_lines(
13
+ "gitignore",
14
+ self.patterns,
15
+ )
16
+
17
+ def add(self, patterns: list[str]):
18
+ if not patterns:
19
+ return
20
+
21
+ self.patterns.extend(patterns)
22
+ self._rebuild()
23
+
24
+ def remove(self, patterns: list[str]):
25
+ if not patterns:
26
+ return
27
+ del self.patterns[-len(patterns):]
28
+ self._rebuild()
29
+
30
+ def should_ignore(self, path: str) -> bool:
31
+ relative_path = Path(path).relative_to(self.repo_root).resolve()
32
+ return self.spec.match_file(relative_path.as_posix())
33
+
34
+
35
+
36
+