minion-code 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.
- examples/advance_tui.py +508 -0
- examples/agent_with_todos.py +165 -0
- examples/file_freshness_example.py +97 -0
- examples/file_watching_example.py +110 -0
- examples/interruptible_tui.py +5 -0
- examples/message_response_children_demo.py +226 -0
- examples/rich_example.py +4 -0
- examples/simple_file_watching.py +57 -0
- examples/simple_tui.py +267 -0
- examples/simple_usage.py +69 -0
- minion_code/__init__.py +16 -0
- minion_code/agents/__init__.py +11 -0
- minion_code/agents/code_agent.py +320 -0
- minion_code/cli.py +502 -0
- minion_code/commands/__init__.py +90 -0
- minion_code/commands/clear_command.py +70 -0
- minion_code/commands/help_command.py +90 -0
- minion_code/commands/history_command.py +104 -0
- minion_code/commands/quit_command.py +32 -0
- minion_code/commands/status_command.py +115 -0
- minion_code/commands/tools_command.py +86 -0
- minion_code/commands/version_command.py +104 -0
- minion_code/components/Message.py +304 -0
- minion_code/components/MessageResponse.py +188 -0
- minion_code/components/PromptInput.py +534 -0
- minion_code/components/__init__.py +29 -0
- minion_code/screens/REPL.py +925 -0
- minion_code/screens/__init__.py +4 -0
- minion_code/services/__init__.py +50 -0
- minion_code/services/event_system.py +108 -0
- minion_code/services/file_freshness_service.py +582 -0
- minion_code/tools/__init__.py +69 -0
- minion_code/tools/bash_tool.py +58 -0
- minion_code/tools/file_edit_tool.py +238 -0
- minion_code/tools/file_read_tool.py +73 -0
- minion_code/tools/file_write_tool.py +36 -0
- minion_code/tools/glob_tool.py +58 -0
- minion_code/tools/grep_tool.py +105 -0
- minion_code/tools/ls_tool.py +65 -0
- minion_code/tools/multi_edit_tool.py +271 -0
- minion_code/tools/python_interpreter_tool.py +105 -0
- minion_code/tools/todo_read_tool.py +100 -0
- minion_code/tools/todo_write_tool.py +234 -0
- minion_code/tools/user_input_tool.py +53 -0
- minion_code/types.py +88 -0
- minion_code/utils/__init__.py +44 -0
- minion_code/utils/mcp_loader.py +211 -0
- minion_code/utils/todo_file_utils.py +110 -0
- minion_code/utils/todo_storage.py +149 -0
- minion_code-0.1.0.dist-info/METADATA +350 -0
- minion_code-0.1.0.dist-info/RECORD +59 -0
- minion_code-0.1.0.dist-info/WHEEL +5 -0
- minion_code-0.1.0.dist-info/entry_points.txt +4 -0
- minion_code-0.1.0.dist-info/licenses/LICENSE +661 -0
- minion_code-0.1.0.dist-info/top_level.txt +3 -0
- tests/__init__.py +1 -0
- tests/test_basic.py +20 -0
- tests/test_readonly_tools.py +102 -0
- tests/test_tools.py +83 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Multi-edit tool based on TypeScript MultiEditTool implementation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any, Optional, List
|
|
10
|
+
from minion.tools import BaseTool
|
|
11
|
+
from minion_code.services import record_file_read, record_file_edit, check_file_freshness
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MultiEditTool(BaseTool):
|
|
15
|
+
"""
|
|
16
|
+
A tool for making multiple edits to a single file atomically.
|
|
17
|
+
Based on the TypeScript MultiEditTool implementation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "multi_edit"
|
|
21
|
+
description = "A tool for making multiple edits to a single file in one operation"
|
|
22
|
+
readonly = False
|
|
23
|
+
|
|
24
|
+
inputs = {
|
|
25
|
+
"file_path": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"description": "The absolute path to the file to modify"
|
|
28
|
+
},
|
|
29
|
+
"edits": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"description": "Array of edit operations to perform sequentially",
|
|
32
|
+
"items": {
|
|
33
|
+
"type": "object",
|
|
34
|
+
"properties": {
|
|
35
|
+
"old_string": {
|
|
36
|
+
"type": "string",
|
|
37
|
+
"description": "The text to replace"
|
|
38
|
+
},
|
|
39
|
+
"new_string": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"description": "The text to replace it with"
|
|
42
|
+
},
|
|
43
|
+
"replace_all": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"description": "Replace all occurrences of old_string (default: false)"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"required": ["old_string", "new_string"]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
output_type = "string"
|
|
53
|
+
|
|
54
|
+
def forward(self, file_path: str, edits: List[Dict[str, Any]]) -> str:
|
|
55
|
+
"""Execute multi-edit operation."""
|
|
56
|
+
try:
|
|
57
|
+
# Validate inputs
|
|
58
|
+
validation_result = self._validate_input(file_path, edits)
|
|
59
|
+
if not validation_result["valid"]:
|
|
60
|
+
return f"Error: {validation_result['message']}"
|
|
61
|
+
|
|
62
|
+
# Apply all edits atomically
|
|
63
|
+
result = self._apply_multi_edit(file_path, edits)
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return f"Error during multi-edit: {str(e)}"
|
|
68
|
+
|
|
69
|
+
def _validate_input(self, file_path: str, edits: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
70
|
+
"""Validate input parameters."""
|
|
71
|
+
|
|
72
|
+
# Check if we have edits
|
|
73
|
+
if not edits or len(edits) == 0:
|
|
74
|
+
return {
|
|
75
|
+
"valid": False,
|
|
76
|
+
"message": "At least one edit operation is required."
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Resolve absolute path
|
|
80
|
+
if not os.path.isabs(file_path):
|
|
81
|
+
file_path = os.path.abspath(file_path)
|
|
82
|
+
|
|
83
|
+
# Check if it's a Jupyter notebook
|
|
84
|
+
if file_path.endswith('.ipynb'):
|
|
85
|
+
return {
|
|
86
|
+
"valid": False,
|
|
87
|
+
"message": "File is a Jupyter Notebook. Use NotebookEdit tool instead."
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# Handle new file creation
|
|
91
|
+
if not os.path.exists(file_path):
|
|
92
|
+
# For new files, ensure parent directory can be created
|
|
93
|
+
parent_dir = os.path.dirname(file_path)
|
|
94
|
+
if parent_dir and not os.path.exists(parent_dir):
|
|
95
|
+
try:
|
|
96
|
+
os.makedirs(parent_dir, exist_ok=True)
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return {
|
|
99
|
+
"valid": False,
|
|
100
|
+
"message": f"Cannot create parent directory: {str(e)}"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
# For new files, first edit must create the file (empty old_string)
|
|
104
|
+
if len(edits) == 0 or edits[0].get("old_string", "") != "":
|
|
105
|
+
return {
|
|
106
|
+
"valid": False,
|
|
107
|
+
"message": "For new files, the first edit must have an empty old_string to create the file content."
|
|
108
|
+
}
|
|
109
|
+
else:
|
|
110
|
+
# For existing files, check freshness
|
|
111
|
+
try:
|
|
112
|
+
freshness_result = check_file_freshness(file_path)
|
|
113
|
+
if freshness_result.conflict:
|
|
114
|
+
return {
|
|
115
|
+
"valid": False,
|
|
116
|
+
"message": "File has been modified since last read. Read it again before editing."
|
|
117
|
+
}
|
|
118
|
+
except Exception:
|
|
119
|
+
# If freshness checking fails, continue with basic validation
|
|
120
|
+
pass
|
|
121
|
+
|
|
122
|
+
# Check if file is binary
|
|
123
|
+
if self._is_binary_file(file_path):
|
|
124
|
+
return {
|
|
125
|
+
"valid": False,
|
|
126
|
+
"message": "Cannot edit binary files."
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Pre-validate that all old_strings exist in the file
|
|
130
|
+
try:
|
|
131
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
132
|
+
current_content = f.read()
|
|
133
|
+
|
|
134
|
+
for i, edit in enumerate(edits):
|
|
135
|
+
old_string = edit.get("old_string", "")
|
|
136
|
+
if old_string != "" and old_string not in current_content:
|
|
137
|
+
return {
|
|
138
|
+
"valid": False,
|
|
139
|
+
"message": f"Edit {i + 1}: String to replace not found in file: \"{old_string[:100]}{'...' if len(old_string) > 100 else ''}\""
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
except UnicodeDecodeError:
|
|
143
|
+
return {
|
|
144
|
+
"valid": False,
|
|
145
|
+
"message": "Cannot read file - appears to be binary or has encoding issues."
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Validate each edit
|
|
149
|
+
for i, edit in enumerate(edits):
|
|
150
|
+
old_string = edit.get("old_string", "")
|
|
151
|
+
new_string = edit.get("new_string", "")
|
|
152
|
+
|
|
153
|
+
if old_string == new_string:
|
|
154
|
+
return {
|
|
155
|
+
"valid": False,
|
|
156
|
+
"message": f"Edit {i + 1}: old_string and new_string cannot be the same"
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {"valid": True}
|
|
160
|
+
|
|
161
|
+
def _apply_multi_edit(self, file_path: str, edits: List[Dict[str, Any]]) -> str:
|
|
162
|
+
"""Apply all edits to the file atomically."""
|
|
163
|
+
|
|
164
|
+
# Resolve absolute path
|
|
165
|
+
if not os.path.isabs(file_path):
|
|
166
|
+
file_path = os.path.abspath(file_path)
|
|
167
|
+
|
|
168
|
+
# Read current file content (or empty for new files)
|
|
169
|
+
file_exists = os.path.exists(file_path)
|
|
170
|
+
|
|
171
|
+
if file_exists:
|
|
172
|
+
try:
|
|
173
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
174
|
+
current_content = f.read()
|
|
175
|
+
except UnicodeDecodeError:
|
|
176
|
+
return "Error: Cannot read file - appears to be binary or has encoding issues."
|
|
177
|
+
else:
|
|
178
|
+
current_content = ""
|
|
179
|
+
# Ensure parent directory exists
|
|
180
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
|
181
|
+
|
|
182
|
+
# Apply all edits sequentially
|
|
183
|
+
modified_content = current_content
|
|
184
|
+
applied_edits = []
|
|
185
|
+
|
|
186
|
+
for i, edit in enumerate(edits):
|
|
187
|
+
old_string = edit.get("old_string", "")
|
|
188
|
+
new_string = edit.get("new_string", "")
|
|
189
|
+
replace_all = edit.get("replace_all", False)
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
result = self._apply_content_edit(
|
|
193
|
+
modified_content, old_string, new_string, replace_all
|
|
194
|
+
)
|
|
195
|
+
modified_content = result["new_content"]
|
|
196
|
+
applied_edits.append({
|
|
197
|
+
"edit_index": i + 1,
|
|
198
|
+
"success": True,
|
|
199
|
+
"old_string": old_string[:100] + ("..." if len(old_string) > 100 else ""),
|
|
200
|
+
"new_string": new_string[:100] + ("..." if len(new_string) > 100 else ""),
|
|
201
|
+
"occurrences": result["occurrences"]
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
except Exception as e:
|
|
205
|
+
# If any edit fails, abort the entire operation
|
|
206
|
+
error_message = str(e)
|
|
207
|
+
return f"Error in edit {i + 1}: {error_message}"
|
|
208
|
+
|
|
209
|
+
# Write the modified content
|
|
210
|
+
try:
|
|
211
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
212
|
+
f.write(modified_content)
|
|
213
|
+
except Exception as e:
|
|
214
|
+
return f"Error writing file: {str(e)}"
|
|
215
|
+
|
|
216
|
+
# Record the file edit
|
|
217
|
+
record_file_edit(file_path, modified_content)
|
|
218
|
+
|
|
219
|
+
# Generate result summary
|
|
220
|
+
operation = "create" if not file_exists else "update"
|
|
221
|
+
summary = f"Successfully applied {len(edits)} edits to {file_path}"
|
|
222
|
+
|
|
223
|
+
# Add details about each edit
|
|
224
|
+
details = []
|
|
225
|
+
for edit_info in applied_edits:
|
|
226
|
+
details.append(
|
|
227
|
+
f"Edit {edit_info['edit_index']}: Replaced {edit_info['occurrences']} occurrence(s)"
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if details:
|
|
231
|
+
summary += "\n" + "\n".join(details)
|
|
232
|
+
|
|
233
|
+
return summary
|
|
234
|
+
|
|
235
|
+
def _apply_content_edit(self, content: str, old_string: str, new_string: str,
|
|
236
|
+
replace_all: bool = False) -> Dict[str, Any]:
|
|
237
|
+
"""Apply a single content edit."""
|
|
238
|
+
|
|
239
|
+
if replace_all:
|
|
240
|
+
# Replace all occurrences
|
|
241
|
+
import re
|
|
242
|
+
# Escape special regex characters in old_string
|
|
243
|
+
escaped_old = re.escape(old_string)
|
|
244
|
+
pattern = re.compile(escaped_old)
|
|
245
|
+
matches = pattern.findall(content)
|
|
246
|
+
occurrences = len(matches)
|
|
247
|
+
new_content = pattern.sub(new_string, content)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"new_content": new_content,
|
|
251
|
+
"occurrences": occurrences
|
|
252
|
+
}
|
|
253
|
+
else:
|
|
254
|
+
# Replace single occurrence
|
|
255
|
+
if old_string in content:
|
|
256
|
+
new_content = content.replace(old_string, new_string, 1) # Replace only first occurrence
|
|
257
|
+
return {
|
|
258
|
+
"new_content": new_content,
|
|
259
|
+
"occurrences": 1
|
|
260
|
+
}
|
|
261
|
+
else:
|
|
262
|
+
raise Exception(f"String not found: {old_string[:50]}...")
|
|
263
|
+
|
|
264
|
+
def _is_binary_file(self, file_path: str) -> bool:
|
|
265
|
+
"""Check if file is binary."""
|
|
266
|
+
try:
|
|
267
|
+
with open(file_path, 'rb') as f:
|
|
268
|
+
chunk = f.read(1024)
|
|
269
|
+
return b'\0' in chunk
|
|
270
|
+
except Exception:
|
|
271
|
+
return False
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Python code execution tool
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import io
|
|
8
|
+
import sys
|
|
9
|
+
from contextlib import redirect_stdout, redirect_stderr
|
|
10
|
+
from minion.tools import BaseTool
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PythonInterpreterTool(BaseTool):
|
|
14
|
+
"""Python code execution tool"""
|
|
15
|
+
|
|
16
|
+
name = "python_interpreter"
|
|
17
|
+
description = "Execute Python code"
|
|
18
|
+
readonly = False # Code execution may modify system state
|
|
19
|
+
inputs = {"code": {"type": "string", "description": "Python code to execute"}}
|
|
20
|
+
output_type = "string"
|
|
21
|
+
|
|
22
|
+
def __init__(self, authorized_imports=None):
|
|
23
|
+
super().__init__()
|
|
24
|
+
if authorized_imports is None:
|
|
25
|
+
self.authorized_imports = [
|
|
26
|
+
"math",
|
|
27
|
+
"random",
|
|
28
|
+
"datetime",
|
|
29
|
+
"json",
|
|
30
|
+
"re",
|
|
31
|
+
"os",
|
|
32
|
+
"sys",
|
|
33
|
+
"collections",
|
|
34
|
+
"itertools",
|
|
35
|
+
"functools",
|
|
36
|
+
"operator",
|
|
37
|
+
]
|
|
38
|
+
else:
|
|
39
|
+
self.authorized_imports = list(
|
|
40
|
+
set(["math", "random", "datetime", "json", "re", "os", "sys"])
|
|
41
|
+
| set(authorized_imports)
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def forward(self, code: str) -> str:
|
|
45
|
+
"""Execute Python code"""
|
|
46
|
+
# Create restricted global environment
|
|
47
|
+
restricted_globals = {
|
|
48
|
+
"__builtins__": {
|
|
49
|
+
"print": print,
|
|
50
|
+
"len": len,
|
|
51
|
+
"str": str,
|
|
52
|
+
"int": int,
|
|
53
|
+
"float": float,
|
|
54
|
+
"bool": bool,
|
|
55
|
+
"list": list,
|
|
56
|
+
"dict": dict,
|
|
57
|
+
"tuple": tuple,
|
|
58
|
+
"set": set,
|
|
59
|
+
"range": range,
|
|
60
|
+
"enumerate": enumerate,
|
|
61
|
+
"zip": zip,
|
|
62
|
+
"sum": sum,
|
|
63
|
+
"max": max,
|
|
64
|
+
"min": min,
|
|
65
|
+
"abs": abs,
|
|
66
|
+
"round": round,
|
|
67
|
+
"sorted": sorted,
|
|
68
|
+
"reversed": reversed,
|
|
69
|
+
"any": any,
|
|
70
|
+
"all": all,
|
|
71
|
+
"__import__": __import__, # Add __import__ function
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Add authorized imports
|
|
76
|
+
for module_name in self.authorized_imports:
|
|
77
|
+
try:
|
|
78
|
+
restricted_globals[module_name] = __import__(module_name)
|
|
79
|
+
except ImportError:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# Capture output
|
|
83
|
+
stdout_capture = io.StringIO()
|
|
84
|
+
stderr_capture = io.StringIO()
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
|
|
88
|
+
exec(code, restricted_globals)
|
|
89
|
+
|
|
90
|
+
stdout_content = stdout_capture.getvalue()
|
|
91
|
+
stderr_content = stderr_capture.getvalue()
|
|
92
|
+
|
|
93
|
+
output_parts = []
|
|
94
|
+
if stdout_content:
|
|
95
|
+
output_parts.append(f"Standard output:\n{stdout_content}")
|
|
96
|
+
if stderr_content:
|
|
97
|
+
output_parts.append(f"Standard error:\n{stderr_content}")
|
|
98
|
+
|
|
99
|
+
if not output_parts:
|
|
100
|
+
output_parts.append("Code executed successfully, no output.")
|
|
101
|
+
|
|
102
|
+
return "\n".join(output_parts)
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
return f"Error executing code: {str(e)}"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Todo Read Tool for viewing current todo items."""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from minion.tools import BaseTool
|
|
5
|
+
from minion.types import AgentState
|
|
6
|
+
|
|
7
|
+
from ..utils.todo_storage import get_todos, TodoStatus
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_todos_display(todos):
|
|
11
|
+
"""Format todos for display."""
|
|
12
|
+
if not todos:
|
|
13
|
+
return "No todos currently"
|
|
14
|
+
|
|
15
|
+
# Sort: [completed, in_progress, pending]
|
|
16
|
+
order = [TodoStatus.COMPLETED, TodoStatus.IN_PROGRESS, TodoStatus.PENDING]
|
|
17
|
+
sorted_todos = sorted(todos, key=lambda t: (order.index(t.status), t.content))
|
|
18
|
+
|
|
19
|
+
# Find the next pending task
|
|
20
|
+
next_pending_index = next(
|
|
21
|
+
(i for i, todo in enumerate(sorted_todos) if todo.status == TodoStatus.PENDING),
|
|
22
|
+
-1
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
lines = []
|
|
26
|
+
for i, todo in enumerate(sorted_todos):
|
|
27
|
+
# Determine checkbox and formatting
|
|
28
|
+
if todo.status == TodoStatus.COMPLETED:
|
|
29
|
+
checkbox = "☒"
|
|
30
|
+
prefix = " ⎿ "
|
|
31
|
+
content = f"~~{todo.content}~~" # Strikethrough for completed
|
|
32
|
+
elif todo.status == TodoStatus.IN_PROGRESS:
|
|
33
|
+
checkbox = "☐"
|
|
34
|
+
prefix = " ⎿ "
|
|
35
|
+
content = f"**{todo.content}**" # Bold for in progress
|
|
36
|
+
else: # pending
|
|
37
|
+
checkbox = "☐"
|
|
38
|
+
prefix = " ⎿ "
|
|
39
|
+
if i == next_pending_index:
|
|
40
|
+
content = f"**{todo.content}**" # Bold for next pending
|
|
41
|
+
else:
|
|
42
|
+
content = todo.content
|
|
43
|
+
|
|
44
|
+
lines.append(f"{prefix}{checkbox} {content}")
|
|
45
|
+
|
|
46
|
+
return "\n".join(lines)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TodoReadTool(BaseTool):
|
|
50
|
+
"""Tool for reading and displaying current todo items."""
|
|
51
|
+
|
|
52
|
+
name = "todo_read"
|
|
53
|
+
description = "View current todo items and their status."
|
|
54
|
+
readonly = True # Read-only tool, does not modify system state
|
|
55
|
+
needs_state = True # Tool needs agent state
|
|
56
|
+
inputs = {}
|
|
57
|
+
output_type = "string"
|
|
58
|
+
|
|
59
|
+
def _get_agent_id(self, state: AgentState) -> str:
|
|
60
|
+
"""Get agent ID from agent state."""
|
|
61
|
+
# Try to get from metadata
|
|
62
|
+
return state.agent.agent_id
|
|
63
|
+
|
|
64
|
+
def forward(self, state: AgentState) -> str:
|
|
65
|
+
"""Execute the todo read operation."""
|
|
66
|
+
try:
|
|
67
|
+
# Get agent ID from agent state
|
|
68
|
+
agent_id = self._get_agent_id(state)
|
|
69
|
+
|
|
70
|
+
# Get current todos
|
|
71
|
+
todos = get_todos(agent_id)
|
|
72
|
+
|
|
73
|
+
if not todos:
|
|
74
|
+
return "No todos currently"
|
|
75
|
+
|
|
76
|
+
# Generate statistics
|
|
77
|
+
stats = {
|
|
78
|
+
'total': len(todos),
|
|
79
|
+
'pending': len([t for t in todos if t.status == TodoStatus.PENDING]),
|
|
80
|
+
'in_progress': len([t for t in todos if t.status == TodoStatus.IN_PROGRESS]),
|
|
81
|
+
'completed': len([t for t in todos if t.status == TodoStatus.COMPLETED])
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Update agent metadata with current todo stats
|
|
85
|
+
state.metadata['current_todo_stats'] = stats
|
|
86
|
+
|
|
87
|
+
# Reset iteration counter since todo tool was used
|
|
88
|
+
state.metadata["iteration_without_todos"] = 0
|
|
89
|
+
|
|
90
|
+
# Format display
|
|
91
|
+
display = format_todos_display(todos)
|
|
92
|
+
|
|
93
|
+
# Generate summary
|
|
94
|
+
summary = f"Found {stats['total']} todo(s): {stats['pending']} pending, {stats['in_progress']} in progress, {stats['completed']} completed"
|
|
95
|
+
|
|
96
|
+
result = f"{summary}\n\n{display}"
|
|
97
|
+
return result
|
|
98
|
+
|
|
99
|
+
except Exception as e:
|
|
100
|
+
return f"Error reading todos: {str(e)}"
|