tunacode-cli 0.0.48__py3-none-any.whl → 0.0.50__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 (45) hide show
  1. api/auth.py +13 -0
  2. api/users.py +8 -0
  3. tunacode/__init__.py +4 -0
  4. tunacode/cli/main.py +4 -0
  5. tunacode/cli/repl.py +39 -6
  6. tunacode/configuration/defaults.py +0 -1
  7. tunacode/constants.py +7 -1
  8. tunacode/core/agents/main.py +268 -245
  9. tunacode/core/agents/utils.py +54 -6
  10. tunacode/core/logging/__init__.py +29 -0
  11. tunacode/core/logging/config.py +57 -0
  12. tunacode/core/logging/formatters.py +48 -0
  13. tunacode/core/logging/handlers.py +83 -0
  14. tunacode/core/logging/logger.py +8 -0
  15. tunacode/core/recursive/__init__.py +18 -0
  16. tunacode/core/recursive/aggregator.py +467 -0
  17. tunacode/core/recursive/budget.py +414 -0
  18. tunacode/core/recursive/decomposer.py +398 -0
  19. tunacode/core/recursive/executor.py +470 -0
  20. tunacode/core/recursive/hierarchy.py +488 -0
  21. tunacode/core/state.py +45 -0
  22. tunacode/exceptions.py +23 -0
  23. tunacode/tools/base.py +7 -1
  24. tunacode/types.py +5 -1
  25. tunacode/ui/completers.py +2 -2
  26. tunacode/ui/console.py +30 -9
  27. tunacode/ui/input.py +2 -1
  28. tunacode/ui/keybindings.py +58 -1
  29. tunacode/ui/logging_compat.py +44 -0
  30. tunacode/ui/output.py +7 -6
  31. tunacode/ui/panels.py +30 -5
  32. tunacode/ui/recursive_progress.py +380 -0
  33. tunacode/utils/retry.py +163 -0
  34. tunacode/utils/security.py +3 -2
  35. tunacode/utils/token_counter.py +1 -2
  36. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/METADATA +2 -2
  37. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/RECORD +41 -29
  38. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/top_level.txt +1 -0
  39. tunacode/core/agents/dspy_integration.py +0 -223
  40. tunacode/core/agents/dspy_tunacode.py +0 -458
  41. tunacode/prompts/dspy_task_planning.md +0 -45
  42. tunacode/prompts/dspy_tool_selection.md +0 -58
  43. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/WHEEL +0 -0
  44. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/entry_points.txt +0 -0
  45. {tunacode_cli-0.0.48.dist-info → tunacode_cli-0.0.50.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,488 @@
1
+ """Module: tunacode.core.recursive.hierarchy
2
+
3
+ Hierarchical task management system for maintaining parent-child relationships and execution state.
4
+ """
5
+
6
+ import logging
7
+ from collections import defaultdict, deque
8
+ from dataclasses import dataclass, field
9
+ from datetime import datetime
10
+ from typing import Any, Dict, List, Optional, Set
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class TaskExecutionContext:
17
+ """Context information for task execution."""
18
+
19
+ task_id: str
20
+ parent_id: Optional[str] = None
21
+ depth: int = 0
22
+ inherited_context: Dict[str, Any] = field(default_factory=dict)
23
+ local_context: Dict[str, Any] = field(default_factory=dict)
24
+ started_at: Optional[datetime] = None
25
+ completed_at: Optional[datetime] = None
26
+
27
+ def get_full_context(self) -> Dict[str, Any]:
28
+ """Get merged context (inherited + local)."""
29
+ return {**self.inherited_context, **self.local_context}
30
+
31
+
32
+ @dataclass
33
+ class TaskRelationship:
34
+ """Represents a relationship between tasks."""
35
+
36
+ parent_id: str
37
+ child_id: str
38
+ relationship_type: str = "subtask" # subtask, dependency, etc.
39
+ metadata: Dict[str, Any] = field(default_factory=dict)
40
+
41
+
42
+ class TaskHierarchy:
43
+ """Manages hierarchical task relationships and execution state."""
44
+
45
+ def __init__(self):
46
+ """Initialize the task hierarchy manager."""
47
+ # Core data structures
48
+ self._tasks: Dict[str, Dict[str, Any]] = {}
49
+ self._parent_to_children: Dict[str, List[str]] = defaultdict(list)
50
+ self._child_to_parent: Dict[str, str] = {}
51
+ self._task_dependencies: Dict[str, Set[str]] = defaultdict(set)
52
+ self._reverse_dependencies: Dict[str, Set[str]] = defaultdict(set)
53
+ self._execution_contexts: Dict[str, TaskExecutionContext] = {}
54
+ self._execution_order: List[str] = []
55
+ self._completed_tasks: Set[str] = set()
56
+ self._failed_tasks: Set[str] = set()
57
+
58
+ def add_task(
59
+ self,
60
+ task_id: str,
61
+ task_data: Dict[str, Any],
62
+ parent_id: Optional[str] = None,
63
+ dependencies: Optional[List[str]] = None,
64
+ ) -> bool:
65
+ """Add a task to the hierarchy.
66
+
67
+ Args:
68
+ task_id: Unique identifier for the task
69
+ task_data: Task information (title, description, etc.)
70
+ parent_id: Optional parent task ID
71
+ dependencies: Optional list of task IDs this task depends on
72
+
73
+ Returns:
74
+ True if task was added successfully, False otherwise
75
+ """
76
+ if task_id in self._tasks:
77
+ logger.warning(f"Task {task_id} already exists")
78
+ return False
79
+
80
+ # Store task data
81
+ self._tasks[task_id] = {
82
+ "id": task_id,
83
+ "parent_id": parent_id,
84
+ "created_at": datetime.now(),
85
+ **task_data,
86
+ }
87
+
88
+ # Set up parent-child relationships
89
+ if parent_id:
90
+ if parent_id not in self._tasks:
91
+ logger.error(f"Parent task {parent_id} does not exist")
92
+ return False
93
+
94
+ self._parent_to_children[parent_id].append(task_id)
95
+ self._child_to_parent[task_id] = parent_id
96
+
97
+ # Set up dependencies
98
+ if dependencies:
99
+ for dep_id in dependencies:
100
+ if dep_id not in self._tasks:
101
+ logger.warning(f"Dependency {dep_id} does not exist yet")
102
+ self.add_dependency(task_id, dep_id)
103
+
104
+ logger.debug(f"Added task {task_id} to hierarchy")
105
+ return True
106
+
107
+ def add_dependency(self, task_id: str, depends_on: str) -> bool:
108
+ """Add a dependency relationship between tasks.
109
+
110
+ Args:
111
+ task_id: Task that has the dependency
112
+ depends_on: Task that must complete first
113
+
114
+ Returns:
115
+ True if dependency was added, False if it would create a cycle
116
+ """
117
+ # Check if adding this would create a cycle
118
+ if self._would_create_cycle(task_id, depends_on):
119
+ logger.error(f"Adding dependency {task_id} -> {depends_on} would create a cycle")
120
+ return False
121
+
122
+ self._task_dependencies[task_id].add(depends_on)
123
+ self._reverse_dependencies[depends_on].add(task_id)
124
+ return True
125
+
126
+ def remove_dependency(self, task_id: str, depends_on: str) -> bool:
127
+ """Remove a dependency relationship.
128
+
129
+ Args:
130
+ task_id: Task that has the dependency
131
+ depends_on: Task to remove from dependencies
132
+
133
+ Returns:
134
+ True if dependency was removed
135
+ """
136
+ if depends_on in self._task_dependencies.get(task_id, set()):
137
+ self._task_dependencies[task_id].remove(depends_on)
138
+ self._reverse_dependencies[depends_on].discard(task_id)
139
+ return True
140
+ return False
141
+
142
+ def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:
143
+ """Get task information.
144
+
145
+ Args:
146
+ task_id: Task identifier
147
+
148
+ Returns:
149
+ Task data or None if not found
150
+ """
151
+ return self._tasks.get(task_id)
152
+
153
+ def get_children(self, parent_id: str) -> List[str]:
154
+ """Get all direct children of a task.
155
+
156
+ Args:
157
+ parent_id: Parent task ID
158
+
159
+ Returns:
160
+ List of child task IDs
161
+ """
162
+ return self._parent_to_children.get(parent_id, []).copy()
163
+
164
+ def get_parent(self, task_id: str) -> Optional[str]:
165
+ """Get the parent of a task.
166
+
167
+ Args:
168
+ task_id: Task ID
169
+
170
+ Returns:
171
+ Parent task ID or None
172
+ """
173
+ return self._child_to_parent.get(task_id)
174
+
175
+ def get_ancestors(self, task_id: str) -> List[str]:
176
+ """Get all ancestors of a task (parent, grandparent, etc.).
177
+
178
+ Args:
179
+ task_id: Task ID
180
+
181
+ Returns:
182
+ List of ancestor IDs from immediate parent to root
183
+ """
184
+ ancestors = []
185
+ current = self._child_to_parent.get(task_id)
186
+
187
+ while current:
188
+ ancestors.append(current)
189
+ current = self._child_to_parent.get(current)
190
+
191
+ return ancestors
192
+
193
+ def get_depth(self, task_id: str) -> int:
194
+ """Get the depth of a task in the hierarchy.
195
+
196
+ Args:
197
+ task_id: Task ID
198
+
199
+ Returns:
200
+ Depth (0 for root tasks)
201
+ """
202
+ return len(self.get_ancestors(task_id))
203
+
204
+ def get_dependencies(self, task_id: str) -> Set[str]:
205
+ """Get tasks that must complete before this task.
206
+
207
+ Args:
208
+ task_id: Task ID
209
+
210
+ Returns:
211
+ Set of dependency task IDs
212
+ """
213
+ return self._task_dependencies.get(task_id, set()).copy()
214
+
215
+ def get_dependents(self, task_id: str) -> Set[str]:
216
+ """Get tasks that depend on this task.
217
+
218
+ Args:
219
+ task_id: Task ID
220
+
221
+ Returns:
222
+ Set of dependent task IDs
223
+ """
224
+ return self._reverse_dependencies.get(task_id, set()).copy()
225
+
226
+ def can_execute(self, task_id: str) -> bool:
227
+ """Check if a task can be executed (all dependencies met).
228
+
229
+ Args:
230
+ task_id: Task ID
231
+
232
+ Returns:
233
+ True if task can be executed
234
+ """
235
+ if task_id not in self._tasks:
236
+ return False
237
+
238
+ # Check if all dependencies are completed
239
+ dependencies = self._task_dependencies.get(task_id, set())
240
+ return all(dep in self._completed_tasks for dep in dependencies)
241
+
242
+ def get_executable_tasks(self) -> List[str]:
243
+ """Get all tasks that can currently be executed.
244
+
245
+ Returns:
246
+ List of task IDs that have all dependencies met
247
+ """
248
+ executable = []
249
+
250
+ for task_id in self._tasks:
251
+ if (
252
+ task_id not in self._completed_tasks
253
+ and task_id not in self._failed_tasks
254
+ and self.can_execute(task_id)
255
+ ):
256
+ executable.append(task_id)
257
+
258
+ return executable
259
+
260
+ def mark_completed(self, task_id: str, result: Any = None) -> None:
261
+ """Mark a task as completed.
262
+
263
+ Args:
264
+ task_id: Task ID
265
+ result: Optional result data
266
+ """
267
+ if task_id in self._tasks:
268
+ self._completed_tasks.add(task_id)
269
+ self._tasks[task_id]["status"] = "completed"
270
+ self._tasks[task_id]["result"] = result
271
+ self._tasks[task_id]["completed_at"] = datetime.now()
272
+
273
+ # Update execution context if exists
274
+ if task_id in self._execution_contexts:
275
+ self._execution_contexts[task_id].completed_at = datetime.now()
276
+
277
+ def mark_failed(self, task_id: str, error: str) -> None:
278
+ """Mark a task as failed.
279
+
280
+ Args:
281
+ task_id: Task ID
282
+ error: Error message
283
+ """
284
+ if task_id in self._tasks:
285
+ self._failed_tasks.add(task_id)
286
+ self._tasks[task_id]["status"] = "failed"
287
+ self._tasks[task_id]["error"] = error
288
+ self._tasks[task_id]["failed_at"] = datetime.now()
289
+
290
+ def create_execution_context(
291
+ self, task_id: str, parent_context: Optional[Dict[str, Any]] = None
292
+ ) -> TaskExecutionContext:
293
+ """Create an execution context for a task.
294
+
295
+ Args:
296
+ task_id: Task ID
297
+ parent_context: Optional parent context to inherit
298
+
299
+ Returns:
300
+ New execution context
301
+ """
302
+ parent_id = self._child_to_parent.get(task_id)
303
+ depth = self.get_depth(task_id)
304
+
305
+ context = TaskExecutionContext(
306
+ task_id=task_id,
307
+ parent_id=parent_id,
308
+ depth=depth,
309
+ inherited_context=parent_context or {},
310
+ started_at=datetime.now(),
311
+ )
312
+
313
+ self._execution_contexts[task_id] = context
314
+ self._execution_order.append(task_id)
315
+
316
+ return context
317
+
318
+ def get_execution_context(self, task_id: str) -> Optional[TaskExecutionContext]:
319
+ """Get the execution context for a task.
320
+
321
+ Args:
322
+ task_id: Task ID
323
+
324
+ Returns:
325
+ Execution context or None
326
+ """
327
+ return self._execution_contexts.get(task_id)
328
+
329
+ def propagate_context(
330
+ self, from_task: str, to_task: str, context_update: Dict[str, Any]
331
+ ) -> None:
332
+ """Propagate context from one task to another.
333
+
334
+ Args:
335
+ from_task: Source task ID (unused, kept for API consistency)
336
+ to_task: Target task ID
337
+ context_update: Context to propagate
338
+ """
339
+ _ = from_task # Unused but kept for API consistency
340
+ if to_task in self._execution_contexts:
341
+ self._execution_contexts[to_task].inherited_context.update(context_update)
342
+
343
+ def aggregate_child_results(self, parent_id: str) -> Dict[str, Any]:
344
+ """Aggregate results from all children of a task.
345
+
346
+ Args:
347
+ parent_id: Parent task ID
348
+
349
+ Returns:
350
+ Aggregated results dictionary
351
+ """
352
+ children = self._parent_to_children.get(parent_id, [])
353
+
354
+ results = {"completed": [], "failed": [], "pending": [], "aggregated_data": {}}
355
+
356
+ for child_id in children:
357
+ child_task = self._tasks.get(child_id, {})
358
+ status = child_task.get("status", "pending")
359
+
360
+ if status == "completed":
361
+ results["completed"].append({"id": child_id, "result": child_task.get("result")})
362
+ elif status == "failed":
363
+ results["failed"].append({"id": child_id, "error": child_task.get("error")})
364
+ else:
365
+ results["pending"].append(child_id)
366
+
367
+ return results
368
+
369
+ def get_execution_path(self, task_id: str) -> List[str]:
370
+ """Get the full execution path from root to this task.
371
+
372
+ Args:
373
+ task_id: Task ID
374
+
375
+ Returns:
376
+ List of task IDs from root to this task
377
+ """
378
+ ancestors = self.get_ancestors(task_id)
379
+ ancestors.reverse() # Root to task order
380
+ ancestors.append(task_id)
381
+ return ancestors
382
+
383
+ def _would_create_cycle(self, task_id: str, depends_on: str) -> bool:
384
+ """Check if adding a dependency would create a cycle.
385
+
386
+ Args:
387
+ task_id: Task that would have the dependency
388
+ depends_on: Task it would depend on
389
+
390
+ Returns:
391
+ True if this would create a cycle
392
+ """
393
+ # BFS to check if we can reach task_id from depends_on
394
+ visited = set()
395
+ queue = deque([depends_on])
396
+
397
+ while queue:
398
+ current = queue.popleft()
399
+ if current == task_id:
400
+ return True
401
+
402
+ if current in visited:
403
+ continue
404
+
405
+ visited.add(current)
406
+
407
+ # Add all tasks that depend on current
408
+ for dependent in self._reverse_dependencies.get(current, []):
409
+ if dependent not in visited:
410
+ queue.append(dependent)
411
+
412
+ return False
413
+
414
+ def get_topological_order(self) -> List[str]:
415
+ """Get a valid execution order respecting all dependencies.
416
+
417
+ Returns:
418
+ List of task IDs in valid execution order
419
+ """
420
+ # Kahn's algorithm for topological sort
421
+ in_degree = {}
422
+ for task_id in self._tasks:
423
+ in_degree[task_id] = len(self._task_dependencies.get(task_id, set()))
424
+
425
+ queue = deque([task_id for task_id, degree in in_degree.items() if degree == 0])
426
+ order = []
427
+
428
+ while queue:
429
+ current = queue.popleft()
430
+ order.append(current)
431
+
432
+ # Reduce in-degree for all dependents
433
+ for dependent in self._reverse_dependencies.get(current, []):
434
+ in_degree[dependent] -= 1
435
+ if in_degree[dependent] == 0:
436
+ queue.append(dependent)
437
+
438
+ # If we couldn't process all tasks, there's a cycle
439
+ if len(order) != len(self._tasks):
440
+ logger.error("Dependency cycle detected")
441
+ # Return partial order
442
+ remaining = [t for t in self._tasks if t not in order]
443
+ order.extend(remaining)
444
+
445
+ return order
446
+
447
+ def visualize_hierarchy(self, show_dependencies: bool = True) -> str:
448
+ """Generate a text visualization of the task hierarchy.
449
+
450
+ Args:
451
+ show_dependencies: Whether to show dependency relationships
452
+
453
+ Returns:
454
+ String representation of the hierarchy
455
+ """
456
+ lines = []
457
+
458
+ # Find root tasks (no parent)
459
+ roots = [task_id for task_id in self._tasks if task_id not in self._child_to_parent]
460
+
461
+ def build_tree(task_id: str, prefix: str = "", is_last: bool = True):
462
+ task = self._tasks[task_id]
463
+ status = task.get("status", "pending")
464
+
465
+ # Build current line
466
+ connector = "└── " if is_last else "├── "
467
+ status_icon = "✓" if status == "completed" else "✗" if status == "failed" else "○"
468
+ line = f"{prefix}{connector}[{status_icon}] {task_id}: {task.get('title', 'Untitled')[:50]}"
469
+
470
+ # Add dependencies if requested
471
+ if show_dependencies:
472
+ deps = self._task_dependencies.get(task_id, set())
473
+ if deps:
474
+ line += f" (deps: {', '.join(deps)})"
475
+
476
+ lines.append(line)
477
+
478
+ # Process children
479
+ children = self._parent_to_children.get(task_id, [])
480
+ for i, child_id in enumerate(children):
481
+ extension = " " if is_last else "│ "
482
+ build_tree(child_id, prefix + extension, i == len(children) - 1)
483
+
484
+ # Build tree for each root
485
+ for i, root_id in enumerate(roots):
486
+ build_tree(root_id, "", i == len(roots) - 1)
487
+
488
+ return "\n".join(lines) if lines else "Empty hierarchy"
tunacode/core/state.py CHANGED
@@ -41,6 +41,10 @@ class SessionState:
41
41
  input_sessions: InputSessions = field(default_factory=dict)
42
42
  current_task: Optional[Any] = None
43
43
  todos: list[TodoItem] = field(default_factory=list)
44
+ # ESC key tracking for double-press functionality
45
+ esc_press_count: int = 0
46
+ last_esc_time: Optional[float] = None
47
+ operation_cancelled: bool = False
44
48
  # Enhanced tracking for thoughts display
45
49
  files_in_context: set[str] = field(default_factory=set)
46
50
  tool_calls: list[dict[str, Any]] = field(default_factory=list)
@@ -68,6 +72,13 @@ class SessionState:
68
72
  "cost": 0.0,
69
73
  }
70
74
  )
75
+ # Recursive execution tracking
76
+ current_recursion_depth: int = 0
77
+ max_recursion_depth: int = 5
78
+ parent_task_id: Optional[str] = None
79
+ task_hierarchy: dict[str, Any] = field(default_factory=dict)
80
+ iteration_budgets: dict[str, int] = field(default_factory=dict)
81
+ recursive_context_stack: list[dict[str, Any]] = field(default_factory=list)
71
82
 
72
83
  def update_token_count(self):
73
84
  """Calculates the total token count from messages and files in context."""
@@ -98,6 +109,40 @@ class StateManager:
98
109
  todo.completed_at = datetime.now()
99
110
  break
100
111
 
112
+ def push_recursive_context(self, context: dict[str, Any]) -> None:
113
+ """Push a new context onto the recursive execution stack."""
114
+ self._session.recursive_context_stack.append(context)
115
+ self._session.current_recursion_depth += 1
116
+
117
+ def pop_recursive_context(self) -> Optional[dict[str, Any]]:
118
+ """Pop the current context from the recursive execution stack."""
119
+ if self._session.recursive_context_stack:
120
+ self._session.current_recursion_depth = max(
121
+ 0, self._session.current_recursion_depth - 1
122
+ )
123
+ return self._session.recursive_context_stack.pop()
124
+ return None
125
+
126
+ def set_task_iteration_budget(self, task_id: str, budget: int) -> None:
127
+ """Set the iteration budget for a specific task."""
128
+ self._session.iteration_budgets[task_id] = budget
129
+
130
+ def get_task_iteration_budget(self, task_id: str) -> int:
131
+ """Get the iteration budget for a specific task."""
132
+ return self._session.iteration_budgets.get(task_id, 10) # Default to 10
133
+
134
+ def can_recurse_deeper(self) -> bool:
135
+ """Check if we can recurse deeper without exceeding limits."""
136
+ return self._session.current_recursion_depth < self._session.max_recursion_depth
137
+
138
+ def reset_recursive_state(self) -> None:
139
+ """Reset all recursive execution state."""
140
+ self._session.current_recursion_depth = 0
141
+ self._session.parent_task_id = None
142
+ self._session.task_hierarchy.clear()
143
+ self._session.iteration_budgets.clear()
144
+ self._session.recursive_context_stack.clear()
145
+
101
146
  def remove_todo(self, todo_id: str) -> None:
102
147
  self._session.todos = [todo for todo in self._session.todos if todo.id != todo_id]
103
148
 
tunacode/exceptions.py CHANGED
@@ -114,3 +114,26 @@ class TooBroadPatternError(ToolExecutionError):
114
114
  f"Pattern '{pattern}' is too broad - no matches found within {timeout_seconds}s. "
115
115
  "Please use a more specific pattern.",
116
116
  )
117
+
118
+
119
+ class ToolBatchingJSONError(TunaCodeError):
120
+ """Raised when JSON parsing fails during tool batching after all retries are exhausted."""
121
+
122
+ def __init__(
123
+ self,
124
+ json_content: str,
125
+ retry_count: int,
126
+ original_error: OriginalError = None,
127
+ ):
128
+ self.json_content = json_content
129
+ self.retry_count = retry_count
130
+ self.original_error = original_error
131
+
132
+ # Truncate JSON content for display if too long
133
+ display_content = json_content[:100] + "..." if len(json_content) > 100 else json_content
134
+
135
+ super().__init__(
136
+ f"The model is having issues with tool batching. "
137
+ f"JSON parsing failed after {retry_count} retries. "
138
+ f"Invalid JSON: {display_content}"
139
+ )
tunacode/tools/base.py CHANGED
@@ -8,6 +8,7 @@ from abc import ABC, abstractmethod
8
8
 
9
9
  from pydantic_ai.exceptions import ModelRetry
10
10
 
11
+ from tunacode.core.logging.logger import get_logger
11
12
  from tunacode.exceptions import FileOperationError, ToolExecutionError
12
13
  from tunacode.types import FilePath, ToolName, ToolResult, UILogger
13
14
 
@@ -22,6 +23,7 @@ class BaseTool(ABC):
22
23
  ui_logger: UI logger instance for displaying messages
23
24
  """
24
25
  self.ui = ui_logger
26
+ self.logger = get_logger(self.__class__.__name__)
25
27
 
26
28
  async def execute(self, *args, **kwargs) -> ToolResult:
27
29
  """Execute the tool with error handling and logging.
@@ -39,14 +41,17 @@ class BaseTool(ABC):
39
41
  ToolExecutionError: Raised for all other errors with structured information
40
42
  """
41
43
  try:
44
+ msg = f"{self.tool_name}({self._format_args(*args, **kwargs)})"
42
45
  if self.ui:
43
- await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
46
+ await self.ui.info(msg)
47
+ self.logger.info(msg)
44
48
  result = await self._execute(*args, **kwargs)
45
49
  return result
46
50
  except ModelRetry as e:
47
51
  # Log as warning and re-raise for pydantic-ai
48
52
  if self.ui:
49
53
  await self.ui.warning(str(e))
54
+ self.logger.warning(f"ModelRetry: {e}")
50
55
  raise
51
56
  except ToolExecutionError:
52
57
  # Already properly formatted, just re-raise
@@ -90,6 +95,7 @@ class BaseTool(ABC):
90
95
  err_msg = f"Error {self._get_error_context(*args, **kwargs)}: {error}"
91
96
  if self.ui:
92
97
  await self.ui.error(err_msg)
98
+ self.logger.error(err_msg)
93
99
 
94
100
  # Raise proper exception instead of returning string
95
101
  raise ToolExecutionError(tool_name=self.tool_name, message=str(error), original_error=error)
tunacode/types.py CHANGED
@@ -13,7 +13,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Prot
13
13
  # Try to import pydantic-ai types if available
14
14
  try:
15
15
  from pydantic_ai import Agent
16
- from pydantic_ai.messages import ModelRequest, ModelResponse, ToolReturnPart
16
+ from pydantic_ai.messages import ModelRequest, ToolReturnPart
17
17
 
18
18
  PydanticAgent = Agent
19
19
  MessagePart = Union[ToolReturnPart, Any]
@@ -49,6 +49,10 @@ SessionId = str
49
49
  DeviceId = str
50
50
  InputSessions = Dict[str, Any]
51
51
 
52
+ # Logging configuration types
53
+ LoggingConfig = Dict[str, Any]
54
+ LoggingEnabled = bool
55
+
52
56
  # =============================================================================
53
57
  # Configuration Types
54
58
  # =============================================================================
tunacode/ui/completers.py CHANGED
@@ -17,7 +17,7 @@ class CommandCompleter(Completer):
17
17
  self.command_registry = command_registry
18
18
 
19
19
  def get_completions(
20
- self, document: Document, complete_event: CompleteEvent
20
+ self, document: Document, _complete_event: CompleteEvent
21
21
  ) -> Iterable[Completion]:
22
22
  """Get completions for slash commands."""
23
23
  # Get the text before cursor
@@ -65,7 +65,7 @@ class FileReferenceCompleter(Completer):
65
65
  """Completer for @file references that provides file path suggestions."""
66
66
 
67
67
  def get_completions(
68
- self, document: Document, complete_event: CompleteEvent
68
+ self, document: Document, _complete_event: CompleteEvent
69
69
  ) -> Iterable[Completion]:
70
70
  """Get completions for @file references."""
71
71
  # Get the word before cursor