tunacode-cli 0.0.55__py3-none-any.whl → 0.0.78.6__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.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (114) hide show
  1. tunacode/cli/commands/__init__.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +2 -3
  3. tunacode/cli/commands/implementations/command_reload.py +48 -0
  4. tunacode/cli/commands/implementations/debug.py +2 -2
  5. tunacode/cli/commands/implementations/development.py +10 -8
  6. tunacode/cli/commands/implementations/model.py +357 -29
  7. tunacode/cli/commands/implementations/quickstart.py +43 -0
  8. tunacode/cli/commands/implementations/system.py +96 -3
  9. tunacode/cli/commands/implementations/template.py +0 -2
  10. tunacode/cli/commands/registry.py +139 -5
  11. tunacode/cli/commands/slash/__init__.py +32 -0
  12. tunacode/cli/commands/slash/command.py +157 -0
  13. tunacode/cli/commands/slash/loader.py +135 -0
  14. tunacode/cli/commands/slash/processor.py +294 -0
  15. tunacode/cli/commands/slash/types.py +93 -0
  16. tunacode/cli/commands/slash/validator.py +400 -0
  17. tunacode/cli/main.py +23 -2
  18. tunacode/cli/repl.py +217 -190
  19. tunacode/cli/repl_components/command_parser.py +38 -4
  20. tunacode/cli/repl_components/error_recovery.py +85 -4
  21. tunacode/cli/repl_components/output_display.py +12 -1
  22. tunacode/cli/repl_components/tool_executor.py +1 -1
  23. tunacode/configuration/defaults.py +12 -3
  24. tunacode/configuration/key_descriptions.py +284 -0
  25. tunacode/configuration/settings.py +0 -1
  26. tunacode/constants.py +12 -40
  27. tunacode/core/agents/__init__.py +43 -2
  28. tunacode/core/agents/agent_components/__init__.py +7 -0
  29. tunacode/core/agents/agent_components/agent_config.py +249 -55
  30. tunacode/core/agents/agent_components/agent_helpers.py +43 -13
  31. tunacode/core/agents/agent_components/node_processor.py +179 -139
  32. tunacode/core/agents/agent_components/response_state.py +123 -6
  33. tunacode/core/agents/agent_components/state_transition.py +116 -0
  34. tunacode/core/agents/agent_components/streaming.py +296 -0
  35. tunacode/core/agents/agent_components/task_completion.py +19 -6
  36. tunacode/core/agents/agent_components/tool_buffer.py +21 -1
  37. tunacode/core/agents/agent_components/tool_executor.py +10 -0
  38. tunacode/core/agents/main.py +522 -370
  39. tunacode/core/agents/main_legact.py +538 -0
  40. tunacode/core/agents/prompts.py +66 -0
  41. tunacode/core/agents/utils.py +29 -121
  42. tunacode/core/code_index.py +83 -29
  43. tunacode/core/setup/__init__.py +0 -2
  44. tunacode/core/setup/config_setup.py +110 -20
  45. tunacode/core/setup/config_wizard.py +230 -0
  46. tunacode/core/setup/coordinator.py +14 -5
  47. tunacode/core/state.py +16 -20
  48. tunacode/core/token_usage/usage_tracker.py +5 -3
  49. tunacode/core/tool_authorization.py +352 -0
  50. tunacode/core/tool_handler.py +67 -40
  51. tunacode/exceptions.py +119 -5
  52. tunacode/prompts/system.xml +751 -0
  53. tunacode/services/mcp.py +125 -7
  54. tunacode/setup.py +5 -25
  55. tunacode/tools/base.py +163 -0
  56. tunacode/tools/bash.py +110 -1
  57. tunacode/tools/glob.py +332 -34
  58. tunacode/tools/grep.py +179 -82
  59. tunacode/tools/grep_components/result_formatter.py +98 -4
  60. tunacode/tools/list_dir.py +132 -2
  61. tunacode/tools/prompts/bash_prompt.xml +72 -0
  62. tunacode/tools/prompts/glob_prompt.xml +45 -0
  63. tunacode/tools/prompts/grep_prompt.xml +98 -0
  64. tunacode/tools/prompts/list_dir_prompt.xml +31 -0
  65. tunacode/tools/prompts/react_prompt.xml +23 -0
  66. tunacode/tools/prompts/read_file_prompt.xml +54 -0
  67. tunacode/tools/prompts/run_command_prompt.xml +64 -0
  68. tunacode/tools/prompts/update_file_prompt.xml +53 -0
  69. tunacode/tools/prompts/write_file_prompt.xml +37 -0
  70. tunacode/tools/react.py +153 -0
  71. tunacode/tools/read_file.py +91 -0
  72. tunacode/tools/run_command.py +114 -0
  73. tunacode/tools/schema_assembler.py +167 -0
  74. tunacode/tools/update_file.py +94 -0
  75. tunacode/tools/write_file.py +86 -0
  76. tunacode/tools/xml_helper.py +83 -0
  77. tunacode/tutorial/__init__.py +9 -0
  78. tunacode/tutorial/content.py +98 -0
  79. tunacode/tutorial/manager.py +182 -0
  80. tunacode/tutorial/steps.py +124 -0
  81. tunacode/types.py +20 -27
  82. tunacode/ui/completers.py +434 -50
  83. tunacode/ui/config_dashboard.py +585 -0
  84. tunacode/ui/console.py +63 -11
  85. tunacode/ui/input.py +20 -3
  86. tunacode/ui/keybindings.py +7 -4
  87. tunacode/ui/model_selector.py +395 -0
  88. tunacode/ui/output.py +40 -19
  89. tunacode/ui/panels.py +212 -43
  90. tunacode/ui/path_heuristics.py +91 -0
  91. tunacode/ui/prompt_manager.py +5 -1
  92. tunacode/ui/tool_ui.py +33 -10
  93. tunacode/utils/api_key_validation.py +93 -0
  94. tunacode/utils/config_comparator.py +340 -0
  95. tunacode/utils/json_utils.py +206 -0
  96. tunacode/utils/message_utils.py +14 -4
  97. tunacode/utils/models_registry.py +593 -0
  98. tunacode/utils/ripgrep.py +332 -9
  99. tunacode/utils/text_utils.py +18 -1
  100. tunacode/utils/user_configuration.py +45 -0
  101. tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
  102. tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
  103. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
  104. tunacode/cli/commands/implementations/todo.py +0 -217
  105. tunacode/context.py +0 -71
  106. tunacode/core/setup/git_safety_setup.py +0 -182
  107. tunacode/prompts/system.md +0 -731
  108. tunacode/tools/read_file_async_poc.py +0 -196
  109. tunacode/tools/todo.py +0 -349
  110. tunacode_cli-0.0.55.dist-info/METADATA +0 -322
  111. tunacode_cli-0.0.55.dist-info/RECORD +0 -126
  112. tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
  113. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
  114. {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,196 +0,0 @@
1
- """
2
- Proof of Concept: Async-optimized read_file tool
3
-
4
- This demonstrates how we can make the read_file tool truly async
5
- by using asyncio.to_thread (Python 3.9+) or run_in_executor.
6
- """
7
-
8
- import asyncio
9
- import os
10
- import sys
11
- from concurrent.futures import ThreadPoolExecutor
12
- from typing import Optional
13
-
14
- from tunacode.constants import (
15
- ERROR_FILE_DECODE,
16
- ERROR_FILE_DECODE_DETAILS,
17
- ERROR_FILE_NOT_FOUND,
18
- ERROR_FILE_TOO_LARGE,
19
- MAX_FILE_SIZE,
20
- MSG_FILE_SIZE_LIMIT,
21
- )
22
- from tunacode.exceptions import ToolExecutionError
23
- from tunacode.tools.base import FileBasedTool
24
- from tunacode.types import ToolResult
25
-
26
- # Shared thread pool for I/O operations
27
- # This avoids creating multiple thread pools
28
- _IO_THREAD_POOL: Optional[ThreadPoolExecutor] = None
29
-
30
-
31
- def get_io_thread_pool() -> ThreadPoolExecutor:
32
- """Get or create the shared I/O thread pool."""
33
- global _IO_THREAD_POOL
34
- if _IO_THREAD_POOL is None:
35
- max_workers = min(32, (os.cpu_count() or 1) * 4)
36
- _IO_THREAD_POOL = ThreadPoolExecutor(
37
- max_workers=max_workers, thread_name_prefix="tunacode-io"
38
- )
39
- return _IO_THREAD_POOL
40
-
41
-
42
- class AsyncReadFileTool(FileBasedTool):
43
- """Async-optimized tool for reading file contents."""
44
-
45
- @property
46
- def tool_name(self) -> str:
47
- return "Read"
48
-
49
- async def _execute(self, filepath: str) -> ToolResult:
50
- """Read the contents of a file asynchronously.
51
-
52
- Args:
53
- filepath: The path to the file to read.
54
-
55
- Returns:
56
- ToolResult: The contents of the file or an error message.
57
-
58
- Raises:
59
- Exception: Any file reading errors
60
- """
61
- # Check file size first (this is fast)
62
- try:
63
- file_size = os.path.getsize(filepath)
64
- except FileNotFoundError:
65
- raise FileNotFoundError(f"File not found: {filepath}")
66
-
67
- if file_size > MAX_FILE_SIZE:
68
- err_msg = ERROR_FILE_TOO_LARGE.format(filepath=filepath) + MSG_FILE_SIZE_LIMIT
69
- if self.ui:
70
- await self.ui.error(err_msg)
71
- raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=None)
72
-
73
- # Read file asynchronously
74
- content = await self._read_file_async(filepath)
75
- return content
76
-
77
- async def _read_file_async(self, filepath: str) -> str:
78
- """Read file contents without blocking the event loop."""
79
-
80
- # Method 1: Using asyncio.to_thread (Python 3.9+)
81
- if sys.version_info >= (3, 9):
82
-
83
- def _read_sync():
84
- with open(filepath, "r", encoding="utf-8") as file:
85
- return file.read()
86
-
87
- try:
88
- return await asyncio.to_thread(_read_sync)
89
- except Exception:
90
- # Re-raise to be handled by _handle_error
91
- raise
92
-
93
- # Method 2: Using run_in_executor (older Python versions)
94
- else:
95
-
96
- def _read_sync(path):
97
- with open(path, "r", encoding="utf-8") as file:
98
- return file.read()
99
-
100
- loop = asyncio.get_event_loop()
101
- executor = get_io_thread_pool()
102
-
103
- try:
104
- return await loop.run_in_executor(executor, _read_sync, filepath)
105
- except Exception:
106
- # Re-raise to be handled by _handle_error
107
- raise
108
-
109
- async def _handle_error(self, error: Exception, filepath: str = None) -> ToolResult:
110
- """Handle errors with specific messages for common cases.
111
-
112
- Raises:
113
- ToolExecutionError: Always raised with structured error information
114
- """
115
- if isinstance(error, FileNotFoundError):
116
- err_msg = ERROR_FILE_NOT_FOUND.format(filepath=filepath)
117
- elif isinstance(error, UnicodeDecodeError):
118
- err_msg = (
119
- ERROR_FILE_DECODE.format(filepath=filepath)
120
- + " "
121
- + ERROR_FILE_DECODE_DETAILS.format(error=error)
122
- )
123
- else:
124
- # Use parent class handling for other errors
125
- await super()._handle_error(error, filepath)
126
- return # super() will raise, this is unreachable
127
-
128
- if self.ui:
129
- await self.ui.error(err_msg)
130
-
131
- raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=error)
132
-
133
-
134
- # Create the async function that maintains the existing interface
135
- async def read_file_async(filepath: str) -> str:
136
- """
137
- Read the contents of a file asynchronously without blocking the event loop.
138
-
139
- This implementation uses thread pool execution to avoid blocking during file I/O,
140
- allowing true parallel execution of multiple file reads.
141
-
142
- Args:
143
- filepath: The path to the file to read.
144
-
145
- Returns:
146
- str: The contents of the file or an error message.
147
- """
148
- tool = AsyncReadFileTool(None) # No UI for pydantic-ai compatibility
149
- try:
150
- return await tool.execute(filepath)
151
- except ToolExecutionError as e:
152
- # Return error message for pydantic-ai compatibility
153
- return str(e)
154
-
155
-
156
- # Benchmarking utilities for testing
157
- async def benchmark_read_performance():
158
- """Benchmark the performance difference between sync and async reads."""
159
- import contextlib
160
- import tempfile
161
- import time
162
-
163
- from tunacode.tools.read_file import read_file as read_file_sync
164
-
165
- # Create some test files using tempfile for secure temporary file creation
166
- test_files = []
167
- for _ in range(10):
168
- with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as temp_file:
169
- temp_file.write("x" * 10000) # 10KB file
170
- test_files.append(temp_file.name)
171
-
172
- # Test synchronous reads (sequential)
173
- start_time = time.time()
174
- for filepath in test_files:
175
- await read_file_sync(filepath)
176
- sync_time = time.time() - start_time
177
-
178
- # Test async reads (parallel)
179
- start_time = time.time()
180
- tasks = [read_file_async(filepath) for filepath in test_files]
181
- await asyncio.gather(*tasks)
182
- async_time = time.time() - start_time
183
-
184
- # Cleanup using safe file removal
185
- for filepath in test_files:
186
- with contextlib.suppress(OSError):
187
- os.unlink(filepath)
188
-
189
- print(f"Synchronous reads: {sync_time:.3f}s")
190
- print(f"Async reads: {async_time:.3f}s")
191
- print(f"Speedup: {sync_time / async_time:.2f}x")
192
-
193
-
194
- if __name__ == "__main__":
195
- # Run benchmark when executed directly
196
- asyncio.run(benchmark_read_performance())
tunacode/tools/todo.py DELETED
@@ -1,349 +0,0 @@
1
- """Todo management tool for agent integration.
2
-
3
- This tool allows the AI agent to manage todo items during task execution.
4
- It provides functionality for creating, updating, and tracking tasks.
5
- """
6
-
7
- import uuid
8
- from datetime import datetime
9
- from typing import List, Literal, Optional, Union
10
-
11
- from pydantic_ai.exceptions import ModelRetry
12
-
13
- from tunacode.constants import (
14
- MAX_TODO_CONTENT_LENGTH,
15
- MAX_TODOS_PER_SESSION,
16
- TODO_PRIORITIES,
17
- TodoPriority,
18
- TodoStatus,
19
- )
20
- from tunacode.types import TodoItem, ToolResult, UILogger
21
-
22
- from .base import BaseTool
23
-
24
-
25
- class TodoTool(BaseTool):
26
- """Tool for managing todo items from the AI agent."""
27
-
28
- def __init__(self, state_manager, ui_logger: UILogger | None = None):
29
- """Initialize the todo tool.
30
-
31
- Args:
32
- state_manager: StateManager instance for accessing todos
33
- ui_logger: UI logger instance for displaying messages
34
- """
35
- super().__init__(ui_logger)
36
- self.state_manager = state_manager
37
-
38
- @property
39
- def tool_name(self) -> str:
40
- return "todo"
41
-
42
- async def _execute(
43
- self,
44
- action: Literal["add", "add_multiple", "update", "complete", "list", "remove"],
45
- content: Optional[Union[str, List[str]]] = None,
46
- todo_id: Optional[str] = None,
47
- status: Optional[Literal["pending", "in_progress", "completed"]] = None,
48
- priority: Optional[Literal["high", "medium", "low"]] = None,
49
- todos: Optional[List[dict]] = None,
50
- ) -> ToolResult:
51
- """Execute todo management actions.
52
-
53
- Args:
54
- action: The action to perform (add, add_multiple, update, complete, list, remove)
55
- content: Content for new todos or updates (can be string or list for add_multiple)
56
- todo_id: ID of existing todo for updates/completion
57
- status: Status to set for updates
58
- priority: Priority to set for new/updated todos
59
- todos: List of todo dictionaries for add_multiple action (format: [{"content": "...", "priority": "..."}])
60
-
61
- Returns:
62
- str: Result message describing what was done
63
-
64
- Raises:
65
- ModelRetry: When invalid parameters are provided
66
- """
67
- if action == "add":
68
- if isinstance(content, list):
69
- raise ModelRetry("Use 'add_multiple' action for adding multiple todos")
70
- return await self._add_todo(content, priority)
71
- elif action == "add_multiple":
72
- return await self._add_multiple_todos(content, todos, priority)
73
- elif action == "update":
74
- if isinstance(content, list):
75
- raise ModelRetry("Cannot update with list content")
76
- return await self._update_todo(todo_id, status, priority, content)
77
- elif action == "complete":
78
- return await self._complete_todo(todo_id)
79
- elif action == "list":
80
- return await self._list_todos()
81
- elif action == "remove":
82
- return await self._remove_todo(todo_id)
83
- else:
84
- raise ModelRetry(
85
- f"Invalid action '{action}'. Must be one of: add, add_multiple, update, complete, list, remove"
86
- )
87
-
88
- async def _add_todo(self, content: Optional[str], priority: Optional[str]) -> ToolResult:
89
- """Add a new todo item."""
90
- if not content:
91
- raise ModelRetry("Content is required when adding a todo")
92
-
93
- # Validate content length
94
- if len(content) > MAX_TODO_CONTENT_LENGTH:
95
- raise ModelRetry(
96
- f"Todo content is too long. Maximum length is {MAX_TODO_CONTENT_LENGTH} characters"
97
- )
98
-
99
- # Check todo limit
100
- if len(self.state_manager.session.todos) >= MAX_TODOS_PER_SESSION:
101
- raise ModelRetry(
102
- f"Cannot add more todos. Maximum of {MAX_TODOS_PER_SESSION} todos allowed per session"
103
- )
104
-
105
- # Generate UUID for guaranteed uniqueness
106
- new_id = f"todo_{uuid.uuid4().hex[:8]}"
107
-
108
- # Default priority if not specified
109
- todo_priority = priority or TodoPriority.MEDIUM
110
- if todo_priority not in [p.value for p in TodoPriority]:
111
- raise ModelRetry(
112
- f"Invalid priority '{todo_priority}'. Must be one of: {', '.join([p.value for p in TodoPriority])}"
113
- )
114
-
115
- new_todo = TodoItem(
116
- id=new_id,
117
- content=content,
118
- status=TodoStatus.PENDING,
119
- priority=todo_priority,
120
- created_at=datetime.now(),
121
- )
122
-
123
- self.state_manager.add_todo(new_todo)
124
- return f"Added todo {new_id}: {content} (priority: {todo_priority})"
125
-
126
- async def _add_multiple_todos(
127
- self,
128
- content: Optional[Union[str, List[str]]],
129
- todos: Optional[List[dict]],
130
- priority: Optional[str],
131
- ) -> ToolResult:
132
- """Add multiple todo items at once."""
133
-
134
- # Handle different input formats
135
- todos_to_add = []
136
-
137
- if todos:
138
- # Structured format: [{"content": "...", "priority": "..."}, ...]
139
- for todo_data in todos:
140
- if not isinstance(todo_data, dict) or "content" not in todo_data:
141
- raise ModelRetry("Each todo must be a dict with 'content' field")
142
- todo_content = todo_data["content"]
143
- todo_priority = todo_data.get("priority", priority or TodoPriority.MEDIUM)
144
- if todo_priority not in TODO_PRIORITIES:
145
- raise ModelRetry(
146
- f"Invalid priority '{todo_priority}'. Must be one of: {', '.join(TODO_PRIORITIES)}"
147
- )
148
- todos_to_add.append((todo_content, todo_priority))
149
- elif isinstance(content, list):
150
- # List of strings format: ["task1", "task2", ...]
151
- default_priority = priority or TodoPriority.MEDIUM
152
- if default_priority not in TODO_PRIORITIES:
153
- raise ModelRetry(
154
- f"Invalid priority '{default_priority}'. Must be one of: {', '.join(TODO_PRIORITIES)}"
155
- )
156
- for task_content in content:
157
- if not isinstance(task_content, str):
158
- raise ModelRetry("All content items must be strings")
159
- todos_to_add.append((task_content, default_priority))
160
- else:
161
- raise ModelRetry(
162
- "For add_multiple, provide either 'todos' list or 'content' as list of strings"
163
- )
164
-
165
- if not todos_to_add:
166
- raise ModelRetry("No todos to add")
167
-
168
- # Check todo limit
169
- current_count = len(self.state_manager.session.todos)
170
- if current_count + len(todos_to_add) > MAX_TODOS_PER_SESSION:
171
- available = MAX_TODOS_PER_SESSION - current_count
172
- raise ModelRetry(
173
- f"Cannot add {len(todos_to_add)} todos. Only {available} slots available (max {MAX_TODOS_PER_SESSION} per session)"
174
- )
175
-
176
- # Add all todos
177
- added_ids = []
178
- for task_content, task_priority in todos_to_add:
179
- # Validate content length
180
- if len(task_content) > MAX_TODO_CONTENT_LENGTH:
181
- raise ModelRetry(
182
- f"Todo content is too long: '{task_content[:50]}...'. Maximum length is {MAX_TODO_CONTENT_LENGTH} characters"
183
- )
184
-
185
- # Generate UUID for guaranteed uniqueness
186
- new_id = f"todo_{uuid.uuid4().hex[:8]}"
187
-
188
- new_todo = TodoItem(
189
- id=new_id,
190
- content=task_content,
191
- status=TodoStatus.PENDING,
192
- priority=task_priority,
193
- created_at=datetime.now(),
194
- )
195
-
196
- self.state_manager.add_todo(new_todo)
197
- added_ids.append(new_id)
198
-
199
- count = len(added_ids)
200
- return f"Added {count} todos (IDs: {', '.join(added_ids)})"
201
-
202
- async def _update_todo(
203
- self,
204
- todo_id: Optional[str],
205
- status: Optional[str],
206
- priority: Optional[str],
207
- content: Optional[str],
208
- ) -> ToolResult:
209
- """Update an existing todo item."""
210
- if not todo_id:
211
- raise ModelRetry("Todo ID is required for updates")
212
-
213
- # Find the todo
214
- todo = None
215
- for t in self.state_manager.session.todos:
216
- if t.id == todo_id:
217
- todo = t
218
- break
219
-
220
- if not todo:
221
- raise ModelRetry(f"Todo with ID '{todo_id}' not found")
222
-
223
- changes = []
224
-
225
- # Update status if provided
226
- if status:
227
- if status not in [s.value for s in TodoStatus]:
228
- raise ModelRetry(
229
- f"Invalid status '{status}'. Must be one of: {', '.join([s.value for s in TodoStatus])}"
230
- )
231
- todo.status = status
232
- if status == TodoStatus.COMPLETED.value and not todo.completed_at:
233
- todo.completed_at = datetime.now()
234
- changes.append(f"status to {status}")
235
-
236
- # Update priority if provided
237
- if priority:
238
- if priority not in [p.value for p in TodoPriority]:
239
- raise ModelRetry(
240
- f"Invalid priority '{priority}'. Must be one of: {', '.join([p.value for p in TodoPriority])}"
241
- )
242
- todo.priority = priority
243
- changes.append(f"priority to {priority}")
244
-
245
- # Update content if provided
246
- if content:
247
- todo.content = content
248
- changes.append(f"content to '{content}'")
249
-
250
- if not changes:
251
- raise ModelRetry(
252
- "At least one of status, priority, or content must be provided for updates"
253
- )
254
-
255
- change_summary = ", ".join(changes)
256
- return f"Updated todo {todo_id}: {change_summary}"
257
-
258
- async def _complete_todo(self, todo_id: Optional[str]) -> ToolResult:
259
- """Mark a todo as completed."""
260
- if not todo_id:
261
- raise ModelRetry("Todo ID is required to mark as complete")
262
-
263
- # Find and update the todo
264
- for todo in self.state_manager.session.todos:
265
- if todo.id == todo_id:
266
- todo.status = TodoStatus.COMPLETED.value
267
- todo.completed_at = datetime.now()
268
- return f"Marked todo {todo_id} as completed: {todo.content}"
269
-
270
- raise ModelRetry(f"Todo with ID '{todo_id}' not found")
271
-
272
- async def _list_todos(self) -> ToolResult:
273
- """List all current todos."""
274
- todos = self.state_manager.session.todos
275
- if not todos:
276
- return "No todos found"
277
-
278
- # Group by status for better organization
279
- pending = [t for t in todos if t.status == TodoStatus.PENDING.value]
280
- in_progress = [t for t in todos if t.status == TodoStatus.IN_PROGRESS.value]
281
- completed = [t for t in todos if t.status == TodoStatus.COMPLETED.value]
282
-
283
- lines = []
284
-
285
- if in_progress:
286
- lines.append("IN PROGRESS:")
287
- for todo in in_progress:
288
- lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
289
-
290
- if pending:
291
- lines.append("\nPENDING:")
292
- for todo in pending:
293
- lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
294
-
295
- if completed:
296
- lines.append("\nCOMPLETED:")
297
- for todo in completed:
298
- lines.append(f" {todo.id}: {todo.content}")
299
-
300
- return "\n".join(lines)
301
-
302
- async def _remove_todo(self, todo_id: Optional[str]) -> ToolResult:
303
- """Remove a todo item."""
304
- if not todo_id:
305
- raise ModelRetry("Todo ID is required to remove a todo")
306
-
307
- # Find the todo first to get its content for the response
308
- todo_content = None
309
- for todo in self.state_manager.session.todos:
310
- if todo.id == todo_id:
311
- todo_content = todo.content
312
- break
313
-
314
- if not todo_content:
315
- raise ModelRetry(f"Todo with ID '{todo_id}' not found")
316
-
317
- self.state_manager.remove_todo(todo_id)
318
- return f"Removed todo {todo_id}: {todo_content}"
319
-
320
- def get_current_todos_sync(self) -> str:
321
- """Get current todos synchronously for system prompt inclusion."""
322
- todos = self.state_manager.session.todos
323
-
324
- if not todos:
325
- return "No todos found"
326
-
327
- # Group by status for better organization
328
- pending = [t for t in todos if t.status == TodoStatus.PENDING.value]
329
- in_progress = [t for t in todos if t.status == TodoStatus.IN_PROGRESS.value]
330
- completed = [t for t in todos if t.status == TodoStatus.COMPLETED.value]
331
-
332
- lines = []
333
-
334
- if in_progress:
335
- lines.append("IN PROGRESS:")
336
- for todo in in_progress:
337
- lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
338
-
339
- if pending:
340
- lines.append("\nPENDING:")
341
- for todo in pending:
342
- lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
343
-
344
- if completed:
345
- lines.append("\nCOMPLETED:")
346
- for todo in completed:
347
- lines.append(f" {todo.id}: {todo.content}")
348
-
349
- return "\n".join(lines)