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.
- tunacode/cli/commands/__init__.py +2 -2
- tunacode/cli/commands/implementations/__init__.py +2 -3
- tunacode/cli/commands/implementations/command_reload.py +48 -0
- tunacode/cli/commands/implementations/debug.py +2 -2
- tunacode/cli/commands/implementations/development.py +10 -8
- tunacode/cli/commands/implementations/model.py +357 -29
- tunacode/cli/commands/implementations/quickstart.py +43 -0
- tunacode/cli/commands/implementations/system.py +96 -3
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +139 -5
- tunacode/cli/commands/slash/__init__.py +32 -0
- tunacode/cli/commands/slash/command.py +157 -0
- tunacode/cli/commands/slash/loader.py +135 -0
- tunacode/cli/commands/slash/processor.py +294 -0
- tunacode/cli/commands/slash/types.py +93 -0
- tunacode/cli/commands/slash/validator.py +400 -0
- tunacode/cli/main.py +23 -2
- tunacode/cli/repl.py +217 -190
- tunacode/cli/repl_components/command_parser.py +38 -4
- tunacode/cli/repl_components/error_recovery.py +85 -4
- tunacode/cli/repl_components/output_display.py +12 -1
- tunacode/cli/repl_components/tool_executor.py +1 -1
- tunacode/configuration/defaults.py +12 -3
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +12 -40
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +249 -55
- tunacode/core/agents/agent_components/agent_helpers.py +43 -13
- tunacode/core/agents/agent_components/node_processor.py +179 -139
- tunacode/core/agents/agent_components/response_state.py +123 -6
- tunacode/core/agents/agent_components/state_transition.py +116 -0
- tunacode/core/agents/agent_components/streaming.py +296 -0
- tunacode/core/agents/agent_components/task_completion.py +19 -6
- tunacode/core/agents/agent_components/tool_buffer.py +21 -1
- tunacode/core/agents/agent_components/tool_executor.py +10 -0
- tunacode/core/agents/main.py +522 -370
- tunacode/core/agents/main_legact.py +538 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/utils.py +29 -121
- tunacode/core/code_index.py +83 -29
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +110 -20
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +14 -5
- tunacode/core/state.py +16 -20
- tunacode/core/token_usage/usage_tracker.py +5 -3
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -40
- tunacode/exceptions.py +119 -5
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +125 -7
- tunacode/setup.py +5 -25
- tunacode/tools/base.py +163 -0
- tunacode/tools/bash.py +110 -1
- tunacode/tools/glob.py +332 -34
- tunacode/tools/grep.py +179 -82
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/prompts/bash_prompt.xml +72 -0
- tunacode/tools/prompts/glob_prompt.xml +45 -0
- tunacode/tools/prompts/grep_prompt.xml +98 -0
- tunacode/tools/prompts/list_dir_prompt.xml +31 -0
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +54 -0
- tunacode/tools/prompts/run_command_prompt.xml +64 -0
- tunacode/tools/prompts/update_file_prompt.xml +53 -0
- tunacode/tools/prompts/write_file_prompt.xml +37 -0
- tunacode/tools/react.py +153 -0
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +114 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/tools/xml_helper.py +83 -0
- tunacode/tutorial/__init__.py +9 -0
- tunacode/tutorial/content.py +98 -0
- tunacode/tutorial/manager.py +182 -0
- tunacode/tutorial/steps.py +124 -0
- tunacode/types.py +20 -27
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +20 -3
- tunacode/ui/keybindings.py +7 -4
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +212 -43
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +5 -1
- tunacode/ui/tool_ui.py +33 -10
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/message_utils.py +14 -4
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/ripgrep.py +332 -9
- tunacode/utils/text_utils.py +18 -1
- tunacode/utils/user_configuration.py +45 -0
- tunacode_cli-0.0.78.6.dist-info/METADATA +260 -0
- tunacode_cli-0.0.78.6.dist-info/RECORD +158 -0
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +1 -2
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -182
- tunacode/prompts/system.md +0 -731
- tunacode/tools/read_file_async_poc.py +0 -196
- tunacode/tools/todo.py +0 -349
- tunacode_cli-0.0.55.dist-info/METADATA +0 -322
- tunacode_cli-0.0.55.dist-info/RECORD +0 -126
- tunacode_cli-0.0.55.dist-info/top_level.txt +0 -1
- {tunacode_cli-0.0.55.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {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)
|