tunacode-cli 0.0.70__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 +0 -2
- tunacode/cli/commands/implementations/__init__.py +0 -3
- 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/system.py +3 -2
- tunacode/cli/commands/implementations/template.py +0 -2
- tunacode/cli/commands/registry.py +8 -7
- tunacode/cli/commands/slash/loader.py +2 -1
- tunacode/cli/commands/slash/validator.py +2 -1
- tunacode/cli/main.py +19 -1
- tunacode/cli/repl.py +90 -229
- tunacode/cli/repl_components/command_parser.py +2 -1
- tunacode/cli/repl_components/error_recovery.py +8 -5
- tunacode/cli/repl_components/output_display.py +1 -10
- tunacode/cli/repl_components/tool_executor.py +1 -13
- tunacode/configuration/defaults.py +2 -2
- tunacode/configuration/key_descriptions.py +284 -0
- tunacode/configuration/settings.py +0 -1
- tunacode/constants.py +6 -42
- tunacode/core/agents/__init__.py +43 -2
- tunacode/core/agents/agent_components/__init__.py +7 -0
- tunacode/core/agents/agent_components/agent_config.py +162 -158
- tunacode/core/agents/agent_components/agent_helpers.py +31 -2
- tunacode/core/agents/agent_components/node_processor.py +180 -146
- 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 -122
- tunacode/core/setup/__init__.py +0 -2
- tunacode/core/setup/config_setup.py +88 -227
- tunacode/core/setup/config_wizard.py +230 -0
- tunacode/core/setup/coordinator.py +2 -1
- tunacode/core/state.py +16 -64
- tunacode/core/token_usage/usage_tracker.py +3 -1
- tunacode/core/tool_authorization.py +352 -0
- tunacode/core/tool_handler.py +67 -60
- tunacode/prompts/system.xml +751 -0
- tunacode/services/mcp.py +97 -1
- tunacode/setup.py +0 -23
- tunacode/tools/base.py +54 -1
- tunacode/tools/bash.py +14 -0
- tunacode/tools/glob.py +4 -2
- tunacode/tools/grep.py +7 -17
- tunacode/tools/prompts/glob_prompt.xml +1 -1
- tunacode/tools/prompts/grep_prompt.xml +1 -0
- tunacode/tools/prompts/list_dir_prompt.xml +1 -1
- tunacode/tools/prompts/react_prompt.xml +23 -0
- tunacode/tools/prompts/read_file_prompt.xml +1 -1
- tunacode/tools/react.py +153 -0
- tunacode/tools/run_command.py +15 -0
- tunacode/types.py +14 -79
- tunacode/ui/completers.py +434 -50
- tunacode/ui/config_dashboard.py +585 -0
- tunacode/ui/console.py +63 -11
- tunacode/ui/input.py +8 -3
- tunacode/ui/keybindings.py +0 -18
- tunacode/ui/model_selector.py +395 -0
- tunacode/ui/output.py +40 -19
- tunacode/ui/panels.py +173 -49
- tunacode/ui/path_heuristics.py +91 -0
- tunacode/ui/prompt_manager.py +1 -20
- tunacode/ui/tool_ui.py +30 -8
- tunacode/utils/api_key_validation.py +93 -0
- tunacode/utils/config_comparator.py +340 -0
- tunacode/utils/models_registry.py +593 -0
- tunacode/utils/text_utils.py +18 -1
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/METADATA +80 -12
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/RECORD +78 -74
- tunacode/cli/commands/implementations/plan.py +0 -50
- tunacode/cli/commands/implementations/todo.py +0 -217
- tunacode/context.py +0 -71
- tunacode/core/setup/git_safety_setup.py +0 -186
- tunacode/prompts/system.md +0 -359
- tunacode/prompts/system.md.bak +0 -487
- tunacode/tools/exit_plan_mode.py +0 -273
- tunacode/tools/present_plan.py +0 -288
- tunacode/tools/prompts/exit_plan_mode_prompt.xml +0 -25
- tunacode/tools/prompts/present_plan_prompt.xml +0 -20
- tunacode/tools/prompts/todo_prompt.xml +0 -96
- tunacode/tools/todo.py +0 -456
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.70.dist-info → tunacode_cli-0.0.78.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<tool_prompt>
|
|
3
|
-
<description>
|
|
4
|
-
Use this tool to create and manage a structured task list for your current coding session. This helps you track progress, organize complex tasks, and demonstrate thoroughness to the user.
|
|
5
|
-
It also helps the user understand the progress of the task and overall progress of their requests.
|
|
6
|
-
|
|
7
|
-
## When to Use This Tool
|
|
8
|
-
Use this tool proactively in these scenarios:
|
|
9
|
-
|
|
10
|
-
1. Complex multi-step tasks - When a task requires 3 or more distinct steps or actions
|
|
11
|
-
2. Non-trivial and complex tasks - Tasks that require careful planning or multiple operations
|
|
12
|
-
3. User explicitly requests todo list - When the user directly asks you to use the todo list
|
|
13
|
-
4. User provides multiple tasks - When users provide a list of things to be done (numbered or comma-separated)
|
|
14
|
-
5. After receiving new instructions - Immediately capture user requirements as todos
|
|
15
|
-
6. When you start working on a task - Mark it as in_progress BEFORE beginning work. Ideally you should only have one todo as in_progress at a time
|
|
16
|
-
7. After completing a task - Mark it as completed and add any new follow-up tasks discovered during implementation
|
|
17
|
-
|
|
18
|
-
## When NOT to Use This Tool
|
|
19
|
-
|
|
20
|
-
Skip using this tool when:
|
|
21
|
-
1. There is only a single, straightforward task
|
|
22
|
-
2. The task is trivial and tracking it provides no organizational benefit
|
|
23
|
-
3. The task can be completed in less than 3 trivial steps
|
|
24
|
-
4. The task is purely conversational or informational
|
|
25
|
-
|
|
26
|
-
NOTE that you should not use this tool if there is only one trivial task to do. In this case you are better off just doing the task directly.
|
|
27
|
-
|
|
28
|
-
## Task States and Management
|
|
29
|
-
|
|
30
|
-
1. **Task States**: Use these states to track progress:
|
|
31
|
-
- pending: Task not yet started
|
|
32
|
-
- in_progress: Currently working on (limit to ONE task at a time)
|
|
33
|
-
- completed: Task finished successfully
|
|
34
|
-
|
|
35
|
-
2. **Task Management**:
|
|
36
|
-
- Update task status in real-time as you work
|
|
37
|
-
- Mark tasks complete IMMEDIATELY after finishing (don't batch completions)
|
|
38
|
-
- Only have ONE task in_progress at any time
|
|
39
|
-
- Complete current tasks before starting new ones
|
|
40
|
-
- Remove tasks that are no longer relevant from the list entirely
|
|
41
|
-
|
|
42
|
-
3. **Task Completion Requirements**:
|
|
43
|
-
- ONLY mark a task as completed when you have FULLY accomplished it
|
|
44
|
-
- If you encounter errors, blockers, or cannot finish, keep the task as in_progress
|
|
45
|
-
- When blocked, create a new task describing what needs to be resolved
|
|
46
|
-
- Never mark a task as completed if:
|
|
47
|
-
- Tests are failing
|
|
48
|
-
- Implementation is partial
|
|
49
|
-
- You encountered unresolved errors
|
|
50
|
-
- You couldn't find necessary files or dependencies
|
|
51
|
-
|
|
52
|
-
4. **Task Breakdown**:
|
|
53
|
-
- Create specific, actionable items
|
|
54
|
-
- Break complex tasks into smaller, manageable steps
|
|
55
|
-
- Use clear, descriptive task names
|
|
56
|
-
|
|
57
|
-
When in doubt, use this tool. Being proactive with task management demonstrates attentiveness and ensures you complete all requirements successfully.
|
|
58
|
-
</description>
|
|
59
|
-
|
|
60
|
-
<parameters>
|
|
61
|
-
<parameter name="todos" required="true">
|
|
62
|
-
<description>The updated todo list</description>
|
|
63
|
-
<type>array</type>
|
|
64
|
-
<items>
|
|
65
|
-
<type>object</type>
|
|
66
|
-
<properties>
|
|
67
|
-
<property name="id" required="true">
|
|
68
|
-
<type>string</type>
|
|
69
|
-
</property>
|
|
70
|
-
<property name="content" required="true">
|
|
71
|
-
<type>string</type>
|
|
72
|
-
<minLength>1</minLength>
|
|
73
|
-
</property>
|
|
74
|
-
<property name="status" required="true">
|
|
75
|
-
<type>string</type>
|
|
76
|
-
<enum>pending</enum>
|
|
77
|
-
<enum>in_progress</enum>
|
|
78
|
-
<enum>completed</enum>
|
|
79
|
-
</property>
|
|
80
|
-
</properties>
|
|
81
|
-
</items>
|
|
82
|
-
</parameter>
|
|
83
|
-
</parameters>
|
|
84
|
-
|
|
85
|
-
<examples>
|
|
86
|
-
<example>
|
|
87
|
-
<title>Create initial todo list</title>
|
|
88
|
-
<command>{"todos": [{"id": "1", "content": "Set up project structure", "status": "pending"}, {"id": "2", "content": "Implement authentication", "status": "pending"}, {"id": "3", "content": "Add tests", "status": "pending"}]}</command>
|
|
89
|
-
</example>
|
|
90
|
-
|
|
91
|
-
<example>
|
|
92
|
-
<title>Update task status</title>
|
|
93
|
-
<command>{"todos": [{"id": "1", "content": "Set up project structure", "status": "completed"}, {"id": "2", "content": "Implement authentication", "status": "in_progress"}, {"id": "3", "content": "Add tests", "status": "pending"}]}</command>
|
|
94
|
-
</example>
|
|
95
|
-
</examples>
|
|
96
|
-
</tool_prompt>
|
tunacode/tools/todo.py
DELETED
|
@@ -1,456 +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 logging
|
|
8
|
-
import uuid
|
|
9
|
-
from datetime import datetime
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
from typing import Any, Dict, List, Literal, Optional, Union
|
|
12
|
-
|
|
13
|
-
import defusedxml.ElementTree as ET
|
|
14
|
-
from pydantic_ai.exceptions import ModelRetry
|
|
15
|
-
|
|
16
|
-
from tunacode.constants import (
|
|
17
|
-
MAX_TODO_CONTENT_LENGTH,
|
|
18
|
-
MAX_TODOS_PER_SESSION,
|
|
19
|
-
TODO_PRIORITIES,
|
|
20
|
-
TodoPriority,
|
|
21
|
-
TodoStatus,
|
|
22
|
-
)
|
|
23
|
-
from tunacode.types import TodoItem, ToolResult, UILogger
|
|
24
|
-
|
|
25
|
-
from .base import BaseTool
|
|
26
|
-
|
|
27
|
-
logger = logging.getLogger(__name__)
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class TodoTool(BaseTool):
|
|
31
|
-
"""Tool for managing todo items from the AI agent."""
|
|
32
|
-
|
|
33
|
-
def _get_base_prompt(self) -> str:
|
|
34
|
-
"""Load and return the base prompt from XML file.
|
|
35
|
-
|
|
36
|
-
Returns:
|
|
37
|
-
str: The loaded prompt from XML or a default prompt
|
|
38
|
-
"""
|
|
39
|
-
try:
|
|
40
|
-
# Load prompt from XML file
|
|
41
|
-
prompt_file = Path(__file__).parent / "prompts" / "todo_prompt.xml"
|
|
42
|
-
if prompt_file.exists():
|
|
43
|
-
tree = ET.parse(prompt_file)
|
|
44
|
-
root = tree.getroot()
|
|
45
|
-
description = root.find("description")
|
|
46
|
-
if description is not None:
|
|
47
|
-
return description.text.strip()
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.warning(f"Failed to load XML prompt for todo: {e}")
|
|
50
|
-
|
|
51
|
-
# Fallback to default prompt
|
|
52
|
-
return """Use this tool to create and manage a structured task list"""
|
|
53
|
-
|
|
54
|
-
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
55
|
-
"""Get the parameters schema for todo tool.
|
|
56
|
-
|
|
57
|
-
Returns:
|
|
58
|
-
Dict containing the JSON schema for tool parameters
|
|
59
|
-
"""
|
|
60
|
-
# Try to load from XML first
|
|
61
|
-
try:
|
|
62
|
-
prompt_file = Path(__file__).parent / "prompts" / "todo_prompt.xml"
|
|
63
|
-
if prompt_file.exists():
|
|
64
|
-
tree = ET.parse(prompt_file)
|
|
65
|
-
root = tree.getroot()
|
|
66
|
-
parameters = root.find("parameters")
|
|
67
|
-
if parameters is not None:
|
|
68
|
-
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
69
|
-
required_fields: List[str] = []
|
|
70
|
-
|
|
71
|
-
for param in parameters.findall("parameter"):
|
|
72
|
-
name = param.get("name")
|
|
73
|
-
required = param.get("required", "false").lower() == "true"
|
|
74
|
-
param_type = param.find("type")
|
|
75
|
-
description = param.find("description")
|
|
76
|
-
|
|
77
|
-
if name and param_type is not None:
|
|
78
|
-
prop = {
|
|
79
|
-
"type": param_type.text.strip(),
|
|
80
|
-
"description": description.text.strip()
|
|
81
|
-
if description is not None
|
|
82
|
-
else "",
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
# Handle array types and nested objects
|
|
86
|
-
if param_type.text.strip() == "array":
|
|
87
|
-
items = param.find("items")
|
|
88
|
-
if items is not None:
|
|
89
|
-
item_type = items.find("type")
|
|
90
|
-
if item_type is not None and item_type.text.strip() == "object":
|
|
91
|
-
# Handle object items
|
|
92
|
-
item_props = {}
|
|
93
|
-
item_properties = items.find("properties")
|
|
94
|
-
if item_properties is not None:
|
|
95
|
-
for item_prop in item_properties.findall("property"):
|
|
96
|
-
prop_name = item_prop.get("name")
|
|
97
|
-
prop_type_elem = item_prop.find("type")
|
|
98
|
-
if prop_name and prop_type_elem is not None:
|
|
99
|
-
item_props[prop_name] = {
|
|
100
|
-
"type": prop_type_elem.text.strip()
|
|
101
|
-
}
|
|
102
|
-
prop["items"] = {"type": "object", "properties": item_props}
|
|
103
|
-
else:
|
|
104
|
-
prop["items"] = {"type": items.text.strip()}
|
|
105
|
-
|
|
106
|
-
schema["properties"][name] = prop
|
|
107
|
-
if required:
|
|
108
|
-
required_fields.append(name)
|
|
109
|
-
|
|
110
|
-
schema["required"] = required_fields
|
|
111
|
-
return schema
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.warning(f"Failed to load parameters from XML for todo: {e}")
|
|
114
|
-
|
|
115
|
-
# Fallback to hardcoded schema
|
|
116
|
-
return {
|
|
117
|
-
"type": "object",
|
|
118
|
-
"properties": {
|
|
119
|
-
"todos": {
|
|
120
|
-
"type": "array",
|
|
121
|
-
"description": "The updated todo list",
|
|
122
|
-
"items": {
|
|
123
|
-
"type": "object",
|
|
124
|
-
"properties": {
|
|
125
|
-
"id": {"type": "string"},
|
|
126
|
-
"content": {"type": "string"},
|
|
127
|
-
"status": {"type": "string"},
|
|
128
|
-
},
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
},
|
|
132
|
-
"required": ["todos"],
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
def __init__(self, state_manager, ui_logger: UILogger | None = None):
|
|
136
|
-
"""Initialize the todo tool.
|
|
137
|
-
|
|
138
|
-
Args:
|
|
139
|
-
state_manager: StateManager instance for accessing todos
|
|
140
|
-
ui_logger: UI logger instance for displaying messages
|
|
141
|
-
"""
|
|
142
|
-
super().__init__(ui_logger)
|
|
143
|
-
self.state_manager = state_manager
|
|
144
|
-
|
|
145
|
-
@property
|
|
146
|
-
def tool_name(self) -> str:
|
|
147
|
-
return "todo"
|
|
148
|
-
|
|
149
|
-
async def _execute(
|
|
150
|
-
self,
|
|
151
|
-
action: Literal["add", "add_multiple", "update", "complete", "list", "remove"],
|
|
152
|
-
content: Optional[Union[str, List[str]]] = None,
|
|
153
|
-
todo_id: Optional[str] = None,
|
|
154
|
-
status: Optional[Literal["pending", "in_progress", "completed"]] = None,
|
|
155
|
-
priority: Optional[Literal["high", "medium", "low"]] = None,
|
|
156
|
-
todos: Optional[List[dict]] = None,
|
|
157
|
-
) -> ToolResult:
|
|
158
|
-
"""Execute todo management actions.
|
|
159
|
-
|
|
160
|
-
Args:
|
|
161
|
-
action: The action to perform (add, add_multiple, update, complete, list, remove)
|
|
162
|
-
content: Content for new todos or updates (can be string or list for add_multiple)
|
|
163
|
-
todo_id: ID of existing todo for updates/completion
|
|
164
|
-
status: Status to set for updates
|
|
165
|
-
priority: Priority to set for new/updated todos
|
|
166
|
-
todos: List of todo dictionaries for add_multiple action (format: [{"content": "...", "priority": "..."}])
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
str: Result message describing what was done
|
|
170
|
-
|
|
171
|
-
Raises:
|
|
172
|
-
ModelRetry: When invalid parameters are provided
|
|
173
|
-
"""
|
|
174
|
-
if action == "add":
|
|
175
|
-
if isinstance(content, list):
|
|
176
|
-
raise ModelRetry("Use 'add_multiple' action for adding multiple todos")
|
|
177
|
-
return await self._add_todo(content, priority)
|
|
178
|
-
elif action == "add_multiple":
|
|
179
|
-
return await self._add_multiple_todos(content, todos, priority)
|
|
180
|
-
elif action == "update":
|
|
181
|
-
if isinstance(content, list):
|
|
182
|
-
raise ModelRetry("Cannot update with list content")
|
|
183
|
-
return await self._update_todo(todo_id, status, priority, content)
|
|
184
|
-
elif action == "complete":
|
|
185
|
-
return await self._complete_todo(todo_id)
|
|
186
|
-
elif action == "list":
|
|
187
|
-
return await self._list_todos()
|
|
188
|
-
elif action == "remove":
|
|
189
|
-
return await self._remove_todo(todo_id)
|
|
190
|
-
else:
|
|
191
|
-
raise ModelRetry(
|
|
192
|
-
f"Invalid action '{action}'. Must be one of: add, add_multiple, update, complete, list, remove"
|
|
193
|
-
)
|
|
194
|
-
|
|
195
|
-
async def _add_todo(self, content: Optional[str], priority: Optional[str]) -> ToolResult:
|
|
196
|
-
"""Add a new todo item."""
|
|
197
|
-
if not content:
|
|
198
|
-
raise ModelRetry("Content is required when adding a todo")
|
|
199
|
-
|
|
200
|
-
# Validate content length
|
|
201
|
-
if len(content) > MAX_TODO_CONTENT_LENGTH:
|
|
202
|
-
raise ModelRetry(
|
|
203
|
-
f"Todo content is too long. Maximum length is {MAX_TODO_CONTENT_LENGTH} characters"
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# Check todo limit
|
|
207
|
-
if len(self.state_manager.session.todos) >= MAX_TODOS_PER_SESSION:
|
|
208
|
-
raise ModelRetry(
|
|
209
|
-
f"Cannot add more todos. Maximum of {MAX_TODOS_PER_SESSION} todos allowed per session"
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
# Generate UUID for guaranteed uniqueness
|
|
213
|
-
new_id = f"todo_{uuid.uuid4().hex[:8]}"
|
|
214
|
-
|
|
215
|
-
# Default priority if not specified
|
|
216
|
-
todo_priority = priority or TodoPriority.MEDIUM
|
|
217
|
-
if todo_priority not in [p.value for p in TodoPriority]:
|
|
218
|
-
raise ModelRetry(
|
|
219
|
-
f"Invalid priority '{todo_priority}'. Must be one of: {', '.join([p.value for p in TodoPriority])}"
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
new_todo = TodoItem(
|
|
223
|
-
id=new_id,
|
|
224
|
-
content=content,
|
|
225
|
-
status=TodoStatus.PENDING,
|
|
226
|
-
priority=todo_priority,
|
|
227
|
-
created_at=datetime.now(),
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
self.state_manager.add_todo(new_todo)
|
|
231
|
-
return f"Added todo {new_id}: {content} (priority: {todo_priority})"
|
|
232
|
-
|
|
233
|
-
async def _add_multiple_todos(
|
|
234
|
-
self,
|
|
235
|
-
content: Optional[Union[str, List[str]]],
|
|
236
|
-
todos: Optional[List[dict]],
|
|
237
|
-
priority: Optional[str],
|
|
238
|
-
) -> ToolResult:
|
|
239
|
-
"""Add multiple todo items at once."""
|
|
240
|
-
|
|
241
|
-
# Handle different input formats
|
|
242
|
-
todos_to_add = []
|
|
243
|
-
|
|
244
|
-
if todos:
|
|
245
|
-
# Structured format: [{"content": "...", "priority": "..."}, ...]
|
|
246
|
-
for todo_data in todos:
|
|
247
|
-
if not isinstance(todo_data, dict) or "content" not in todo_data:
|
|
248
|
-
raise ModelRetry("Each todo must be a dict with 'content' field")
|
|
249
|
-
todo_content = todo_data["content"]
|
|
250
|
-
todo_priority = todo_data.get("priority", priority or TodoPriority.MEDIUM)
|
|
251
|
-
if todo_priority not in TODO_PRIORITIES:
|
|
252
|
-
raise ModelRetry(
|
|
253
|
-
f"Invalid priority '{todo_priority}'. Must be one of: {', '.join(TODO_PRIORITIES)}"
|
|
254
|
-
)
|
|
255
|
-
todos_to_add.append((todo_content, todo_priority))
|
|
256
|
-
elif isinstance(content, list):
|
|
257
|
-
# List of strings format: ["task1", "task2", ...]
|
|
258
|
-
default_priority = priority or TodoPriority.MEDIUM
|
|
259
|
-
if default_priority not in TODO_PRIORITIES:
|
|
260
|
-
raise ModelRetry(
|
|
261
|
-
f"Invalid priority '{default_priority}'. Must be one of: {', '.join(TODO_PRIORITIES)}"
|
|
262
|
-
)
|
|
263
|
-
for task_content in content:
|
|
264
|
-
if not isinstance(task_content, str):
|
|
265
|
-
raise ModelRetry("All content items must be strings")
|
|
266
|
-
todos_to_add.append((task_content, default_priority))
|
|
267
|
-
else:
|
|
268
|
-
raise ModelRetry(
|
|
269
|
-
"For add_multiple, provide either 'todos' list or 'content' as list of strings"
|
|
270
|
-
)
|
|
271
|
-
|
|
272
|
-
if not todos_to_add:
|
|
273
|
-
raise ModelRetry("No todos to add")
|
|
274
|
-
|
|
275
|
-
# Check todo limit
|
|
276
|
-
current_count = len(self.state_manager.session.todos)
|
|
277
|
-
if current_count + len(todos_to_add) > MAX_TODOS_PER_SESSION:
|
|
278
|
-
available = MAX_TODOS_PER_SESSION - current_count
|
|
279
|
-
raise ModelRetry(
|
|
280
|
-
f"Cannot add {len(todos_to_add)} todos. Only {available} slots available (max {MAX_TODOS_PER_SESSION} per session)"
|
|
281
|
-
)
|
|
282
|
-
|
|
283
|
-
# Add all todos
|
|
284
|
-
added_ids = []
|
|
285
|
-
for task_content, task_priority in todos_to_add:
|
|
286
|
-
# Validate content length
|
|
287
|
-
if len(task_content) > MAX_TODO_CONTENT_LENGTH:
|
|
288
|
-
raise ModelRetry(
|
|
289
|
-
f"Todo content is too long: '{task_content[:50]}...'. Maximum length is {MAX_TODO_CONTENT_LENGTH} characters"
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
# Generate UUID for guaranteed uniqueness
|
|
293
|
-
new_id = f"todo_{uuid.uuid4().hex[:8]}"
|
|
294
|
-
|
|
295
|
-
new_todo = TodoItem(
|
|
296
|
-
id=new_id,
|
|
297
|
-
content=task_content,
|
|
298
|
-
status=TodoStatus.PENDING,
|
|
299
|
-
priority=task_priority,
|
|
300
|
-
created_at=datetime.now(),
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
self.state_manager.add_todo(new_todo)
|
|
304
|
-
added_ids.append(new_id)
|
|
305
|
-
|
|
306
|
-
count = len(added_ids)
|
|
307
|
-
return f"Added {count} todos (IDs: {', '.join(added_ids)})"
|
|
308
|
-
|
|
309
|
-
async def _update_todo(
|
|
310
|
-
self,
|
|
311
|
-
todo_id: Optional[str],
|
|
312
|
-
status: Optional[str],
|
|
313
|
-
priority: Optional[str],
|
|
314
|
-
content: Optional[str],
|
|
315
|
-
) -> ToolResult:
|
|
316
|
-
"""Update an existing todo item."""
|
|
317
|
-
if not todo_id:
|
|
318
|
-
raise ModelRetry("Todo ID is required for updates")
|
|
319
|
-
|
|
320
|
-
# Find the todo
|
|
321
|
-
todo = None
|
|
322
|
-
for t in self.state_manager.session.todos:
|
|
323
|
-
if t.id == todo_id:
|
|
324
|
-
todo = t
|
|
325
|
-
break
|
|
326
|
-
|
|
327
|
-
if not todo:
|
|
328
|
-
raise ModelRetry(f"Todo with ID '{todo_id}' not found")
|
|
329
|
-
|
|
330
|
-
changes = []
|
|
331
|
-
|
|
332
|
-
# Update status if provided
|
|
333
|
-
if status:
|
|
334
|
-
if status not in [s.value for s in TodoStatus]:
|
|
335
|
-
raise ModelRetry(
|
|
336
|
-
f"Invalid status '{status}'. Must be one of: {', '.join([s.value for s in TodoStatus])}"
|
|
337
|
-
)
|
|
338
|
-
todo.status = status
|
|
339
|
-
if status == TodoStatus.COMPLETED.value and not todo.completed_at:
|
|
340
|
-
todo.completed_at = datetime.now()
|
|
341
|
-
changes.append(f"status to {status}")
|
|
342
|
-
|
|
343
|
-
# Update priority if provided
|
|
344
|
-
if priority:
|
|
345
|
-
if priority not in [p.value for p in TodoPriority]:
|
|
346
|
-
raise ModelRetry(
|
|
347
|
-
f"Invalid priority '{priority}'. Must be one of: {', '.join([p.value for p in TodoPriority])}"
|
|
348
|
-
)
|
|
349
|
-
todo.priority = priority
|
|
350
|
-
changes.append(f"priority to {priority}")
|
|
351
|
-
|
|
352
|
-
# Update content if provided
|
|
353
|
-
if content:
|
|
354
|
-
todo.content = content
|
|
355
|
-
changes.append(f"content to '{content}'")
|
|
356
|
-
|
|
357
|
-
if not changes:
|
|
358
|
-
raise ModelRetry(
|
|
359
|
-
"At least one of status, priority, or content must be provided for updates"
|
|
360
|
-
)
|
|
361
|
-
|
|
362
|
-
change_summary = ", ".join(changes)
|
|
363
|
-
return f"Updated todo {todo_id}: {change_summary}"
|
|
364
|
-
|
|
365
|
-
async def _complete_todo(self, todo_id: Optional[str]) -> ToolResult:
|
|
366
|
-
"""Mark a todo as completed."""
|
|
367
|
-
if not todo_id:
|
|
368
|
-
raise ModelRetry("Todo ID is required to mark as complete")
|
|
369
|
-
|
|
370
|
-
# Find and update the todo
|
|
371
|
-
for todo in self.state_manager.session.todos:
|
|
372
|
-
if todo.id == todo_id:
|
|
373
|
-
todo.status = TodoStatus.COMPLETED.value
|
|
374
|
-
todo.completed_at = datetime.now()
|
|
375
|
-
return f"Marked todo {todo_id} as completed: {todo.content}"
|
|
376
|
-
|
|
377
|
-
raise ModelRetry(f"Todo with ID '{todo_id}' not found")
|
|
378
|
-
|
|
379
|
-
async def _list_todos(self) -> ToolResult:
|
|
380
|
-
"""List all current todos."""
|
|
381
|
-
todos = self.state_manager.session.todos
|
|
382
|
-
if not todos:
|
|
383
|
-
return "No todos found"
|
|
384
|
-
|
|
385
|
-
# Group by status for better organization
|
|
386
|
-
pending = [t for t in todos if t.status == TodoStatus.PENDING.value]
|
|
387
|
-
in_progress = [t for t in todos if t.status == TodoStatus.IN_PROGRESS.value]
|
|
388
|
-
completed = [t for t in todos if t.status == TodoStatus.COMPLETED.value]
|
|
389
|
-
|
|
390
|
-
lines = []
|
|
391
|
-
|
|
392
|
-
if in_progress:
|
|
393
|
-
lines.append("IN PROGRESS:")
|
|
394
|
-
for todo in in_progress:
|
|
395
|
-
lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
|
|
396
|
-
|
|
397
|
-
if pending:
|
|
398
|
-
lines.append("\nPENDING:")
|
|
399
|
-
for todo in pending:
|
|
400
|
-
lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
|
|
401
|
-
|
|
402
|
-
if completed:
|
|
403
|
-
lines.append("\nCOMPLETED:")
|
|
404
|
-
for todo in completed:
|
|
405
|
-
lines.append(f" {todo.id}: {todo.content}")
|
|
406
|
-
|
|
407
|
-
return "\n".join(lines)
|
|
408
|
-
|
|
409
|
-
async def _remove_todo(self, todo_id: Optional[str]) -> ToolResult:
|
|
410
|
-
"""Remove a todo item."""
|
|
411
|
-
if not todo_id:
|
|
412
|
-
raise ModelRetry("Todo ID is required to remove a todo")
|
|
413
|
-
|
|
414
|
-
# Find the todo first to get its content for the response
|
|
415
|
-
todo_content = None
|
|
416
|
-
for todo in self.state_manager.session.todos:
|
|
417
|
-
if todo.id == todo_id:
|
|
418
|
-
todo_content = todo.content
|
|
419
|
-
break
|
|
420
|
-
|
|
421
|
-
if not todo_content:
|
|
422
|
-
raise ModelRetry(f"Todo with ID '{todo_id}' not found")
|
|
423
|
-
|
|
424
|
-
self.state_manager.remove_todo(todo_id)
|
|
425
|
-
return f"Removed todo {todo_id}: {todo_content}"
|
|
426
|
-
|
|
427
|
-
def get_current_todos_sync(self) -> str:
|
|
428
|
-
"""Get current todos synchronously for system prompt inclusion."""
|
|
429
|
-
todos = self.state_manager.session.todos
|
|
430
|
-
|
|
431
|
-
if not todos:
|
|
432
|
-
return "No todos found"
|
|
433
|
-
|
|
434
|
-
# Group by status for better organization
|
|
435
|
-
pending = [t for t in todos if t.status == TodoStatus.PENDING.value]
|
|
436
|
-
in_progress = [t for t in todos if t.status == TodoStatus.IN_PROGRESS.value]
|
|
437
|
-
completed = [t for t in todos if t.status == TodoStatus.COMPLETED.value]
|
|
438
|
-
|
|
439
|
-
lines = []
|
|
440
|
-
|
|
441
|
-
if in_progress:
|
|
442
|
-
lines.append("IN PROGRESS:")
|
|
443
|
-
for todo in in_progress:
|
|
444
|
-
lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
|
|
445
|
-
|
|
446
|
-
if pending:
|
|
447
|
-
lines.append("\nPENDING:")
|
|
448
|
-
for todo in pending:
|
|
449
|
-
lines.append(f" {todo.id}: {todo.content} (priority: {todo.priority})")
|
|
450
|
-
|
|
451
|
-
if completed:
|
|
452
|
-
lines.append("\nCOMPLETED:")
|
|
453
|
-
for todo in completed:
|
|
454
|
-
lines.append(f" {todo.id}: {todo.content}")
|
|
455
|
-
|
|
456
|
-
return "\n".join(lines)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|