todo-agent 0.3.1__py3-none-any.whl → 0.3.3__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.
@@ -2,17 +2,16 @@
2
2
  Command-line interface for todo.sh LLM agent.
3
3
  """
4
4
 
5
+ import readline
5
6
  from typing import Optional
6
7
 
7
- try:
8
- import readline
9
-
10
- from rich.align import Align
11
- from rich.console import Console
12
- from rich.live import Live
13
- from rich.spinner import Spinner
14
- from rich.text import Text
8
+ from rich.align import Align
9
+ from rich.console import Console, Group
10
+ from rich.live import Live
11
+ from rich.spinner import Spinner
12
+ from rich.text import Text
15
13
 
14
+ try:
16
15
  from todo_agent.core.todo_manager import TodoManager
17
16
  from todo_agent.infrastructure.config import Config
18
17
  from todo_agent.infrastructure.inference import Inference
@@ -25,6 +24,7 @@ try:
25
24
  TableFormatter,
26
25
  TaskFormatter,
27
26
  )
27
+ from todo_agent.interface.progress import ToolCallProgress
28
28
  from todo_agent.interface.tools import ToolCallHandler
29
29
  except ImportError:
30
30
  from core.todo_manager import TodoManager # type: ignore[no-redef]
@@ -39,12 +39,8 @@ except ImportError:
39
39
  TableFormatter,
40
40
  TaskFormatter,
41
41
  )
42
+ from interface.progress import ToolCallProgress # type: ignore[no-redef]
42
43
  from interface.tools import ToolCallHandler # type: ignore[no-redef]
43
- from rich.align import Align
44
- from rich.console import Console
45
- from rich.live import Live
46
- from rich.spinner import Spinner
47
- from rich.text import Text
48
44
 
49
45
 
50
46
  class CLI:
@@ -105,6 +101,113 @@ class CLI:
105
101
  initial_spinner = self._create_thinking_spinner("Thinking...")
106
102
  return Live(initial_spinner, console=self.console, refresh_per_second=10)
107
103
 
104
+ def _create_tool_call_spinner(
105
+ self, progress_description: str, sequence: int = 0, total_sequences: int = 0
106
+ ) -> Group:
107
+ """
108
+ Create a multi-line spinner showing tool call progress.
109
+
110
+ Args:
111
+ progress_description: User-friendly description of what the tool is doing
112
+ sequence: Current sequence number
113
+ total_sequences: Total number of sequences
114
+
115
+ Returns:
116
+ Group object with spinner and optional sequence info
117
+ """
118
+ # Line 1: Main progress with spinner
119
+ main_line = Spinner("dots", text=Text(progress_description, style="cyan"))
120
+
121
+ # Line 2: Sequence progress (show current sequence even if we don't know total)
122
+ # if sequence > 0:
123
+ # if total_sequences > 0:
124
+ # sequence_text = Text(f"Sequence {sequence}/{total_sequences}", style="dim")
125
+ # else:
126
+ # sequence_text = Text(f"Sequence {sequence}", style="dim")
127
+ # return Group(main_line, sequence_text)
128
+
129
+ return Group(main_line)
130
+
131
+ def _create_completion_spinner(self, thinking_time: float) -> Spinner:
132
+ """
133
+ Create completion spinner with timing.
134
+
135
+ Args:
136
+ thinking_time: Total thinking time in seconds
137
+
138
+ Returns:
139
+ Spinner object showing completion
140
+ """
141
+ return Spinner(
142
+ "dots", text=Text(f"✅ Complete ({thinking_time:.1f}s)", style="green")
143
+ )
144
+
145
+ def _create_cli_progress_callback(self, live_display: Live) -> ToolCallProgress:
146
+ """
147
+ Create a CLI-specific progress callback for tool call tracking.
148
+
149
+ Args:
150
+ live_display: The live display to update
151
+
152
+ Returns:
153
+ ToolCallProgress implementation for CLI
154
+ """
155
+
156
+ class CLIProgressCallback(ToolCallProgress):
157
+ def __init__(self, cli: CLI, live: Live):
158
+ self.cli = cli
159
+ self.live = live
160
+ self.current_sequence = 0
161
+ self.total_sequences = 0
162
+
163
+ def on_thinking_start(self) -> None:
164
+ """Show initial thinking spinner."""
165
+ spinner = self.cli._create_thinking_spinner(
166
+ "🤔 Analyzing your request..."
167
+ )
168
+ self.live.update(spinner)
169
+
170
+ def on_tool_call_start(
171
+ self,
172
+ tool_name: str,
173
+ progress_description: str,
174
+ sequence: int,
175
+ total_sequences: int,
176
+ ) -> None:
177
+ """Show tool execution progress."""
178
+ self.current_sequence = sequence
179
+ self.total_sequences = total_sequences
180
+
181
+ # Create multi-line spinner
182
+ spinner = self.cli._create_tool_call_spinner(
183
+ progress_description=progress_description,
184
+ sequence=sequence,
185
+ total_sequences=total_sequences,
186
+ )
187
+ self.live.update(spinner)
188
+
189
+ def on_tool_call_complete(
190
+ self, tool_name: str, success: bool, duration: float
191
+ ) -> None:
192
+ """Tool completion - no action needed."""
193
+ pass
194
+
195
+ def on_sequence_complete(self, sequence: int, total_sequences: int) -> None:
196
+ """Show sequence completion."""
197
+ spinner = self.cli._create_tool_call_spinner(
198
+ progress_description=f"🔄 Sequence {sequence} complete",
199
+ sequence=sequence,
200
+ total_sequences=total_sequences,
201
+ )
202
+ self.live.update(spinner)
203
+
204
+ def on_thinking_complete(self, total_time: float) -> None:
205
+ """Show completion spinner."""
206
+ spinner = self.cli._create_completion_spinner(total_time)
207
+ self.live.update(spinner)
208
+
209
+ return CLIProgressCallback(self, live_display)
210
+
108
211
  def _print_header(self) -> None:
109
212
  """Print the application header with unicode borders."""
110
213
  header_panel = PanelFormatter.create_header_panel()
@@ -207,7 +310,8 @@ class CLI:
207
310
  if user_input.lower() == "list":
208
311
  self.logger.debug("User requested task list")
209
312
  try:
210
- output = self.todo_shell.list_tasks()
313
+ # Use suppress_color=False for interactive display to preserve colors
314
+ output = self.todo_shell.list_tasks(suppress_color=False)
211
315
  formatted_output = TaskFormatter.format_task_list(output)
212
316
  task_panel = PanelFormatter.create_task_panel(formatted_output)
213
317
  self.console.print(task_panel)
@@ -222,7 +326,8 @@ class CLI:
222
326
  if user_input.lower() == "done":
223
327
  self.logger.debug("User requested completed task list")
224
328
  try:
225
- output = self.todo_shell.list_completed()
329
+ # Use suppress_color=False for interactive display to preserve colors
330
+ output = self.todo_shell.list_completed(suppress_color=False)
226
331
  formatted_output = TaskFormatter.format_completed_tasks(output)
227
332
  task_panel = PanelFormatter.create_task_panel(
228
333
  formatted_output, title="✅ Completed Tasks"
@@ -246,8 +351,8 @@ class CLI:
246
351
 
247
352
  # Get memory usage
248
353
  # DISABLED FOR NOW
249
- # memory_usage = self._get_memory_usage()
250
- memory_usage = None
354
+ memory_usage = self._get_memory_usage()
355
+ # memory_usage = None
251
356
 
252
357
  # Create response panel with memory usage
253
358
  response_panel = PanelFormatter.create_response_panel(
@@ -277,8 +382,13 @@ class CLI:
277
382
  # Show thinking spinner during LLM processing
278
383
  with self._get_thinking_live() as live:
279
384
  try:
280
- # Process request through inference engine
281
- response, thinking_time = self.inference.process_request(user_input)
385
+ # Create progress callback for tool call tracking
386
+ progress_callback = self._create_cli_progress_callback(live)
387
+
388
+ # Process request through inference engine with progress tracking
389
+ response, thinking_time = self.inference.process_request(
390
+ user_input, progress_callback
391
+ )
282
392
 
283
393
  # Update spinner with completion message and thinking time
284
394
  live.update(
@@ -14,6 +14,31 @@ from rich.text import Text
14
14
  CLI_WIDTH = 100
15
15
  PANEL_WIDTH = CLI_WIDTH - 2 # Leave 2 characters for borders
16
16
 
17
+ # Provider error message mapping
18
+ PROVIDER_ERROR_MESSAGES = {
19
+ "malformed_response": "I got a confusing response from my AI service. Please try again, or type 'clear' to reset our conversation.",
20
+ "malformed_tool_call": "I received a malformed request. Please try again, or type 'clear' to reset our conversation.",
21
+ "rate_limit": "I'm a bit overwhelmed right now. Please wait a moment and try again, or type 'clear' to start fresh.",
22
+ "auth_error": "I can't connect to my AI service. Please check your configuration, or type 'clear' to reset.",
23
+ "timeout": "The request took too long. Please try again, or type 'clear' to reset our conversation.",
24
+ "general_error": "Something went wrong with my AI service. Please try again, or type 'clear' to reset our conversation.",
25
+ }
26
+
27
+
28
+ def get_provider_error_message(error_type: str) -> str:
29
+ """
30
+ Get user-friendly error message for provider errors.
31
+
32
+ Args:
33
+ error_type: The type of provider error
34
+
35
+ Returns:
36
+ User-friendly error message with recovery suggestion
37
+ """
38
+ return PROVIDER_ERROR_MESSAGES.get(
39
+ error_type, PROVIDER_ERROR_MESSAGES["general_error"]
40
+ )
41
+
17
42
 
18
43
  class TaskFormatter:
19
44
  """Formats task-related output with unicode characters and consistent styling."""
@@ -0,0 +1,69 @@
1
+ """
2
+ Progress tracking interface for tool call execution.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+
7
+
8
+ class ToolCallProgress(ABC):
9
+ """Abstract interface for tool call progress tracking."""
10
+
11
+ @abstractmethod
12
+ def on_thinking_start(self) -> None:
13
+ """Called when LLM starts thinking."""
14
+ pass
15
+
16
+ @abstractmethod
17
+ def on_tool_call_start(
18
+ self,
19
+ tool_name: str,
20
+ progress_description: str,
21
+ sequence: int,
22
+ total_sequences: int,
23
+ ) -> None:
24
+ """Called when a tool call starts."""
25
+ pass
26
+
27
+ @abstractmethod
28
+ def on_tool_call_complete(
29
+ self, tool_name: str, success: bool, duration: float
30
+ ) -> None:
31
+ """Called when a tool call completes (optional - no action needed)."""
32
+ pass
33
+
34
+ @abstractmethod
35
+ def on_sequence_complete(self, sequence: int, total_sequences: int) -> None:
36
+ """Called when a tool call sequence completes."""
37
+ pass
38
+
39
+ @abstractmethod
40
+ def on_thinking_complete(self, total_time: float) -> None:
41
+ """Called when thinking is complete."""
42
+ pass
43
+
44
+
45
+ class NoOpProgress(ToolCallProgress):
46
+ """No-operation implementation for when progress tracking is not needed."""
47
+
48
+ def on_thinking_start(self) -> None:
49
+ pass
50
+
51
+ def on_tool_call_start(
52
+ self,
53
+ tool_name: str,
54
+ progress_description: str,
55
+ sequence: int,
56
+ total_sequences: int,
57
+ ) -> None:
58
+ pass
59
+
60
+ def on_tool_call_complete(
61
+ self, tool_name: str, success: bool, duration: float
62
+ ) -> None:
63
+ pass
64
+
65
+ def on_sequence_complete(self, sequence: int, total_sequences: int) -> None:
66
+ pass
67
+
68
+ def on_thinking_complete(self, total_time: float) -> None:
69
+ pass
@@ -10,22 +10,24 @@ Discovery Tools (Call FIRST):
10
10
  - list_completed_tasks() - List all completed tasks from done.txt
11
11
 
12
12
  Task Management Tools:
13
- - add_task(description, priority?, project?, context?, due?, recurring?) - Add new task to todo.txt
13
+ - add_task(description, priority?, project?, context?, due?, duration?) - Add new task to todo.txt
14
14
  - complete_task(task_number) - Mark task as complete by line number
15
15
  - replace_task(task_number, new_description) - Replace entire task content
16
16
  - append_to_task(task_number, text) - Add text to end of existing task
17
17
  - prepend_to_task(task_number, text) - Add text to beginning of existing task
18
18
  - delete_task(task_number, term?) - Delete entire task or remove specific term
19
+ - created_completed_task(description, completion_date?, project?, context?) - Create task and immediately mark as completed
19
20
 
20
21
  Priority Management Tools:
21
22
  - set_priority(task_number, priority) - Set or change task priority (A-Z)
22
23
  - remove_priority(task_number) - Remove priority from task
24
+
25
+ Task Modification Tools:
23
26
  - set_due_date(task_number, due_date) - Set or update due date for a task by intelligently rewriting it (use empty string to remove due date)
24
27
  - set_context(task_number, context) - Set or update context for a task by intelligently rewriting it (use empty string to remove context)
25
28
  - set_project(task_number, projects) - Set or update projects for a task by intelligently rewriting it (handles array of projects with add/remove operations)
26
29
 
27
30
  Utility Tools:
28
- - get_overview() - Show task statistics and summary
29
31
  - move_task(task_number, destination, source?) - Move task between files
30
32
  - archive_tasks() - Archive completed tasks from todo.txt to done.txt
31
33
  - get_calendar(month, year) - Get calendar for specific month and year
@@ -68,6 +70,7 @@ class ToolCallHandler:
68
70
  ),
69
71
  "parameters": {"type": "object", "properties": {}, "required": []},
70
72
  },
73
+ "progress_description": "📁 Discovering available projects...",
71
74
  },
72
75
  {
73
76
  "type": "function",
@@ -83,6 +86,7 @@ class ToolCallHandler:
83
86
  ),
84
87
  "parameters": {"type": "object", "properties": {}, "required": []},
85
88
  },
89
+ "progress_description": "📍 Finding available contexts...",
86
90
  },
87
91
  {
88
92
  "type": "function",
@@ -102,6 +106,7 @@ class ToolCallHandler:
102
106
  ),
103
107
  "parameters": {"type": "object", "properties": {}, "required": []},
104
108
  },
109
+ "progress_description": "📋 Scanning your task list...",
105
110
  },
106
111
  {
107
112
  "type": "function",
@@ -120,6 +125,7 @@ class ToolCallHandler:
120
125
  ),
121
126
  "parameters": {"type": "object", "properties": {}, "required": []},
122
127
  },
128
+ "progress_description": "✅ Checking completion history...",
123
129
  },
124
130
  {
125
131
  "type": "function",
@@ -166,10 +172,6 @@ class ToolCallHandler:
166
172
  "type": "string",
167
173
  "description": "Optional due date in YYYY-MM-DD format. Use parse_date() tool to convert natural language expressions like 'tomorrow', 'next week', 'by Friday' to YYYY-MM-DD format",
168
174
  },
169
- "recurring": {
170
- "type": "string",
171
- "description": "Optional recurring pattern in rec:frequency[:interval] format (e.g., 'rec:daily', 'rec:weekly:2', 'rec:monthly'). Use for tasks that repeat automatically.",
172
- },
173
175
  "duration": {
174
176
  "type": "string",
175
177
  "description": "Optional duration estimate in format: minutes (e.g., '30m'), hours (e.g., '2h'), or days (e.g., '1d'). Use for time planning and task prioritization.",
@@ -178,6 +180,7 @@ class ToolCallHandler:
178
180
  "required": ["description"],
179
181
  },
180
182
  },
183
+ "progress_description": "✨ Creating new task: {description}...",
181
184
  },
182
185
  {
183
186
  "type": "function",
@@ -201,6 +204,7 @@ class ToolCallHandler:
201
204
  "required": ["task_number"],
202
205
  },
203
206
  },
207
+ "progress_description": "🎯 Marking task #{task_number} as complete...",
204
208
  },
205
209
  {
206
210
  "type": "function",
@@ -228,6 +232,7 @@ class ToolCallHandler:
228
232
  "required": ["task_number", "new_description"],
229
233
  },
230
234
  },
235
+ "progress_description": "✏️ Updating task #{task_number} with new description...",
231
236
  },
232
237
  {
233
238
  "type": "function",
@@ -249,14 +254,15 @@ class ToolCallHandler:
249
254
  "type": "integer",
250
255
  "description": "The line number of the task to modify (required)",
251
256
  },
252
- "text": {
257
+ "text_to_append": {
253
258
  "type": "string",
254
- "description": "The text to append to the task (required)",
259
+ "description": "Text to add to the end of the task (required)",
255
260
  },
256
261
  },
257
- "required": ["task_number", "text"],
262
+ "required": ["task_number", "text_to_append"],
258
263
  },
259
264
  },
265
+ "progress_description": "📝 Adding notes to task #{task_number}...",
260
266
  },
261
267
  {
262
268
  "type": "function",
@@ -283,6 +289,7 @@ class ToolCallHandler:
283
289
  "required": ["task_number", "text"],
284
290
  },
285
291
  },
292
+ "progress_description": "📝 Adding prefix to task #{task_number}...",
286
293
  },
287
294
  {
288
295
  "type": "function",
@@ -313,6 +320,7 @@ class ToolCallHandler:
313
320
  "required": ["task_number"],
314
321
  },
315
322
  },
323
+ "progress_description": "🗑️ Deleting task #{task_number}...",
316
324
  },
317
325
  {
318
326
  "type": "function",
@@ -340,6 +348,7 @@ class ToolCallHandler:
340
348
  "required": ["task_number", "priority"],
341
349
  },
342
350
  },
351
+ "progress_description": "🏷️ Setting priority {priority} for task #{task_number}...",
343
352
  },
344
353
  {
345
354
  "type": "function",
@@ -363,6 +372,7 @@ class ToolCallHandler:
363
372
  "required": ["task_number"],
364
373
  },
365
374
  },
375
+ "progress_description": "🏷️ Removing priority from task #{task_number}...",
366
376
  },
367
377
  {
368
378
  "type": "function",
@@ -394,6 +404,7 @@ class ToolCallHandler:
394
404
  "required": ["task_number", "due_date"],
395
405
  },
396
406
  },
407
+ "progress_description": "📅 Setting due date {due_date} for task #{task_number}...",
397
408
  },
398
409
  {
399
410
  "type": "function",
@@ -425,6 +436,7 @@ class ToolCallHandler:
425
436
  "required": ["task_number", "context"],
426
437
  },
427
438
  },
439
+ "progress_description": "📍 Setting context {context} for task #{task_number}...",
428
440
  },
429
441
  {
430
442
  "type": "function",
@@ -460,17 +472,7 @@ class ToolCallHandler:
460
472
  "required": ["task_number", "projects"],
461
473
  },
462
474
  },
463
- },
464
- {
465
- "type": "function",
466
- "function": {
467
- "name": "get_overview",
468
- "description": (
469
- "Show task statistics and summary. Use this when user asks for "
470
- "an overview, summary, or statistics about their tasks."
471
- ),
472
- "parameters": {"type": "object", "properties": {}, "required": []},
473
- },
475
+ "progress_description": "🏷️ Setting project tags for task #{task_number}...",
474
476
  },
475
477
  {
476
478
  "type": "function",
@@ -502,6 +504,7 @@ class ToolCallHandler:
502
504
  "required": ["task_number", "destination"],
503
505
  },
504
506
  },
507
+ "progress_description": "📦 Moving task #{task_number} to {destination}...",
505
508
  },
506
509
  {
507
510
  "type": "function",
@@ -514,6 +517,7 @@ class ToolCallHandler:
514
517
  ),
515
518
  "parameters": {"type": "object", "properties": {}, "required": []},
516
519
  },
520
+ "progress_description": "📦 Archiving completed tasks...",
517
521
  },
518
522
  {
519
523
  "type": "function",
@@ -540,6 +544,7 @@ class ToolCallHandler:
540
544
  "required": ["date_expression"],
541
545
  },
542
546
  },
547
+ "progress_description": "📅 Converting date expression '{date_expression}'...",
543
548
  },
544
549
  {
545
550
  "type": "function",
@@ -575,6 +580,73 @@ class ToolCallHandler:
575
580
  "required": ["month", "year"],
576
581
  },
577
582
  },
583
+ "progress_description": "📅 Generating calendar for {month}/{year}...",
584
+ },
585
+ {
586
+ "type": "function",
587
+ "function": {
588
+ "name": "created_completed_task",
589
+ "description": (
590
+ "Create a task and immediately mark it as completed. "
591
+ "USE CASE: Call this when user says they completed something on a specific date (e.g., 'I did the laundry today', 'I finished the report yesterday', 'I cleaned the garage last week') "
592
+ "and you have already researched existing tasks to determine no match exists. "
593
+ "WORKFLOW: 1) Use list_tasks() to search for existing tasks, 2) Use list_completed_tasks() to verify it's not already done, "
594
+ "3) If no match found, call this tool to create and complete the task in one operation. "
595
+ "STRATEGIC CONTEXT: This is a convenience tool for the common pattern of 'I did X on [date]' - "
596
+ "it creates a task with the specified completion date and immediately marks it complete. "
597
+ "The LLM should handle the research and decision-making about whether to use this tool."
598
+ ),
599
+ "parameters": {
600
+ "type": "object",
601
+ "properties": {
602
+ "description": {
603
+ "type": "string",
604
+ "description": "The task description of what was completed (required)",
605
+ },
606
+ "completion_date": {
607
+ "type": "string",
608
+ "description": "Optional completion date in YYYY-MM-DD format (defaults to today)",
609
+ },
610
+ "project": {
611
+ "type": "string",
612
+ "description": "Optional project name (without the + symbol) for new task creation",
613
+ },
614
+ "context": {
615
+ "type": "string",
616
+ "description": "Optional context name (without the @ symbol) for new task creation",
617
+ },
618
+ },
619
+ "required": ["description"],
620
+ },
621
+ },
622
+ "progress_description": "✅ Creating completed task: {description}...",
623
+ },
624
+ {
625
+ "type": "function",
626
+ "function": {
627
+ "name": "restore_completed_task",
628
+ "description": (
629
+ "Restore a completed task from done.txt back to todo.txt, making it active again. "
630
+ "USE CASE: Call this when user wants to reactivate a previously completed task. "
631
+ "WORKFLOW: 1) Use list_completed_tasks() to find the completed task to restore, "
632
+ "2) Call restore_completed_task() with the task number from done.txt. "
633
+ "NOT FOR: Creating new tasks, completing tasks, or any other task operations. "
634
+ "This tool ONLY restores existing completed tasks to active status. "
635
+ "IMPORTANT: Use list_completed_tasks() first to find the correct task number "
636
+ "if user doesn't specify it."
637
+ ),
638
+ "parameters": {
639
+ "type": "object",
640
+ "properties": {
641
+ "task_number": {
642
+ "type": "integer",
643
+ "description": "The line number of the completed task in done.txt to restore (required)",
644
+ }
645
+ },
646
+ "required": ["task_number"],
647
+ },
648
+ },
649
+ "progress_description": "🔄 Restoring completed task #{task_number}...",
578
650
  },
579
651
  ]
580
652
 
@@ -731,8 +803,54 @@ class ToolCallHandler:
731
803
 
732
804
  def execute_tool(self, tool_call: Dict[str, Any]) -> Dict[str, Any]:
733
805
  """Execute a tool call and return the result."""
734
- tool_name = tool_call["function"]["name"]
735
- arguments = tool_call["function"]["arguments"]
806
+ # Validate tool call structure
807
+ if not isinstance(tool_call, dict):
808
+ return { # type: ignore[unreachable]
809
+ "tool_call_id": "unknown",
810
+ "name": "unknown",
811
+ "output": "ERROR: Invalid tool call format",
812
+ "error": True,
813
+ "error_type": "malformed_tool_call",
814
+ "error_details": "Tool call is not a dictionary",
815
+ "user_message": "I received a malformed request. Please try again, or type 'clear' to reset our conversation.",
816
+ }
817
+
818
+ if "function" not in tool_call:
819
+ return {
820
+ "tool_call_id": tool_call.get("id", "unknown"),
821
+ "name": "unknown",
822
+ "output": "ERROR: Tool call missing function definition",
823
+ "error": True,
824
+ "error_type": "malformed_tool_call",
825
+ "error_details": "Tool call missing function field",
826
+ "user_message": "I received a malformed request. Please try again, or type 'clear' to reset our conversation.",
827
+ }
828
+
829
+ function = tool_call["function"]
830
+ if not isinstance(function, dict):
831
+ return {
832
+ "tool_call_id": tool_call.get("id", "unknown"),
833
+ "name": "unknown",
834
+ "output": "ERROR: Function definition is not a dictionary",
835
+ "error": True,
836
+ "error_type": "malformed_tool_call",
837
+ "error_details": "Function field is not a dictionary",
838
+ "user_message": "I received a malformed request. Please try again, or type 'clear' to reset our conversation.",
839
+ }
840
+
841
+ tool_name = function.get("name")
842
+ if not tool_name:
843
+ return {
844
+ "tool_call_id": tool_call.get("id", "unknown"),
845
+ "name": "unknown",
846
+ "output": "ERROR: Tool call missing function name",
847
+ "error": True,
848
+ "error_type": "malformed_tool_call",
849
+ "error_details": "Function missing name field",
850
+ "user_message": "I received a malformed request. Please try again, or type 'clear' to reset our conversation.",
851
+ }
852
+
853
+ arguments = function.get("arguments", {})
736
854
  tool_call_id = tool_call.get("id", "unknown")
737
855
 
738
856
  # Handle arguments that might be a string (JSON) or already a dict
@@ -779,11 +897,12 @@ class ToolCallHandler:
779
897
  "set_due_date": self.todo_manager.set_due_date,
780
898
  "set_context": self.todo_manager.set_context,
781
899
  "set_project": self.todo_manager.set_project,
782
- "get_overview": self.todo_manager.get_overview,
783
900
  "move_task": self.todo_manager.move_task,
784
901
  "archive_tasks": self.todo_manager.archive_tasks,
785
902
  "parse_date": self._parse_date,
786
903
  "get_calendar": self._get_calendar,
904
+ "created_completed_task": self.todo_manager.created_completed_task,
905
+ "restore_completed_task": self.todo_manager.restore_completed_task,
787
906
  }
788
907
 
789
908
  if tool_name not in method_map:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.3.1
3
+ Version: 0.3.3
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -68,7 +68,7 @@ todo-agent "show my work tasks"
68
68
 
69
69
  **Get intelligent insights** beyond basic task lists. It organizes tasks strategically, suggests priorities, and recommends optimal timing based on your patterns.
70
70
 
71
- **Work smarter** with automatic duplicate detection, recurring task handling, and calendar-aware scheduling.
71
+ **Work smarter** with automatic duplicate detection and calendar-aware scheduling.
72
72
 
73
73
  **Choose your privacy** - use cloud AI (OpenRouter) or run locally (Ollama).
74
74
 
@@ -163,7 +163,7 @@ todo-agent "what tasks are blocking other work?"
163
163
  ### Natural Language Intelligence
164
164
  ```bash
165
165
  todo-agent "add dentist appointment next Monday"
166
- todo-agent "set up recurring daily vitamin reminder"
166
+
167
167
  todo-agent "move all completed tasks to archive"
168
168
  todo-agent "show me tasks I can do from home"
169
169
  ```