tsugite-cli 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.
Files changed (101) hide show
  1. tsugite/__init__.py +6 -0
  2. tsugite/agent_composition.py +163 -0
  3. tsugite/agent_inheritance.py +479 -0
  4. tsugite/agent_preparation.py +236 -0
  5. tsugite/agent_runner/__init__.py +45 -0
  6. tsugite/agent_runner/helpers.py +106 -0
  7. tsugite/agent_runner/history_integration.py +248 -0
  8. tsugite/agent_runner/metrics.py +100 -0
  9. tsugite/agent_runner/runner.py +1879 -0
  10. tsugite/agent_runner/validation.py +70 -0
  11. tsugite/agent_utils.py +167 -0
  12. tsugite/attachments/__init__.py +65 -0
  13. tsugite/attachments/auto_context.py +199 -0
  14. tsugite/attachments/base.py +34 -0
  15. tsugite/attachments/file.py +51 -0
  16. tsugite/attachments/inline.py +31 -0
  17. tsugite/attachments/storage.py +178 -0
  18. tsugite/attachments/url.py +59 -0
  19. tsugite/attachments/youtube.py +101 -0
  20. tsugite/benchmark/__init__.py +62 -0
  21. tsugite/benchmark/config.py +183 -0
  22. tsugite/benchmark/core.py +292 -0
  23. tsugite/benchmark/discovery.py +377 -0
  24. tsugite/benchmark/evaluators.py +671 -0
  25. tsugite/benchmark/execution.py +657 -0
  26. tsugite/benchmark/metrics.py +204 -0
  27. tsugite/benchmark/reports.py +420 -0
  28. tsugite/benchmark/utils.py +288 -0
  29. tsugite/builtin_agents/chat-assistant.md +53 -0
  30. tsugite/builtin_agents/default.md +140 -0
  31. tsugite/builtin_agents.py +5 -0
  32. tsugite/cache.py +195 -0
  33. tsugite/cli/__init__.py +1042 -0
  34. tsugite/cli/agents.py +148 -0
  35. tsugite/cli/attachments.py +193 -0
  36. tsugite/cli/benchmark.py +663 -0
  37. tsugite/cli/cache.py +113 -0
  38. tsugite/cli/config.py +272 -0
  39. tsugite/cli/helpers.py +534 -0
  40. tsugite/cli/history.py +193 -0
  41. tsugite/cli/init.py +387 -0
  42. tsugite/cli/mcp.py +193 -0
  43. tsugite/cli/tools.py +419 -0
  44. tsugite/config.py +204 -0
  45. tsugite/console.py +48 -0
  46. tsugite/constants.py +21 -0
  47. tsugite/core/__init__.py +19 -0
  48. tsugite/core/agent.py +774 -0
  49. tsugite/core/executor.py +300 -0
  50. tsugite/core/memory.py +67 -0
  51. tsugite/core/tools.py +271 -0
  52. tsugite/docker_cli.py +270 -0
  53. tsugite/events/__init__.py +55 -0
  54. tsugite/events/base.py +46 -0
  55. tsugite/events/bus.py +62 -0
  56. tsugite/events/events.py +224 -0
  57. tsugite/exceptions.py +40 -0
  58. tsugite/history/__init__.py +29 -0
  59. tsugite/history/index.py +210 -0
  60. tsugite/history/models.py +106 -0
  61. tsugite/history/storage.py +157 -0
  62. tsugite/mcp_client.py +219 -0
  63. tsugite/mcp_config.py +174 -0
  64. tsugite/md_agents.py +751 -0
  65. tsugite/models.py +257 -0
  66. tsugite/renderer.py +151 -0
  67. tsugite/shell_tool_config.py +265 -0
  68. tsugite/templates/assistant.md +14 -0
  69. tsugite/tools/__init__.py +265 -0
  70. tsugite/tools/agents.py +312 -0
  71. tsugite/tools/edit_strategies.py +393 -0
  72. tsugite/tools/fs.py +329 -0
  73. tsugite/tools/http.py +239 -0
  74. tsugite/tools/interactive.py +430 -0
  75. tsugite/tools/shell.py +129 -0
  76. tsugite/tools/shell_tools.py +214 -0
  77. tsugite/tools/tasks.py +339 -0
  78. tsugite/tsugite.py +7 -0
  79. tsugite/ui/__init__.py +46 -0
  80. tsugite/ui/base.py +638 -0
  81. tsugite/ui/chat.py +265 -0
  82. tsugite/ui/chat.tcss +92 -0
  83. tsugite/ui/chat_history.py +286 -0
  84. tsugite/ui/helpers.py +102 -0
  85. tsugite/ui/jsonl.py +125 -0
  86. tsugite/ui/live_template.py +529 -0
  87. tsugite/ui/plain.py +419 -0
  88. tsugite/ui/textual_chat.py +642 -0
  89. tsugite/ui/textual_handler.py +225 -0
  90. tsugite/ui/widgets/__init__.py +6 -0
  91. tsugite/ui/widgets/base_scroll_log.py +27 -0
  92. tsugite/ui/widgets/message_list.py +121 -0
  93. tsugite/ui/widgets/thought_log.py +80 -0
  94. tsugite/ui_context.py +90 -0
  95. tsugite/utils.py +367 -0
  96. tsugite/xdg.py +104 -0
  97. tsugite_cli-0.3.3.dist-info/METADATA +325 -0
  98. tsugite_cli-0.3.3.dist-info/RECORD +101 -0
  99. tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
  100. tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
  101. tsugite_cli-0.3.3.dist-info/licenses/LICENSE +235 -0
@@ -0,0 +1,214 @@
1
+ """Shell-based custom tools for Tsugite agents.
2
+
3
+ This module provides a lightweight system for defining tools that wrap shell commands,
4
+ allowing users to create custom tools without writing Python code.
5
+ """
6
+
7
+ from typing import Any, Dict, List, Optional
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ from tsugite.tools import tool
12
+ from tsugite.utils import execute_shell_command
13
+
14
+
15
+ class ShellToolParameter(BaseModel):
16
+ """Definition of a parameter for a shell tool."""
17
+
18
+ model_config = ConfigDict(
19
+ extra="forbid",
20
+ )
21
+
22
+ name: str
23
+ type: str # "str", "int", "bool", "float"
24
+ description: str = ""
25
+ required: bool = False
26
+ default: Any = None
27
+ flag: Optional[str] = None # For boolean flags (e.g., "-s" for case_sensitive)
28
+
29
+ def validate(self, value: Any) -> Any:
30
+ """Validate and convert parameter value to correct type."""
31
+ if value is None:
32
+ if self.required:
33
+ raise ValueError(f"Required parameter '{self.name}' is missing")
34
+ return self.default
35
+
36
+ # Type conversion
37
+ if self.type == "str":
38
+ return str(value)
39
+ elif self.type == "int":
40
+ try:
41
+ return int(value)
42
+ except (ValueError, TypeError):
43
+ raise ValueError(f"Parameter '{self.name}' must be an integer")
44
+ elif self.type == "float":
45
+ try:
46
+ return float(value)
47
+ except (ValueError, TypeError):
48
+ raise ValueError(f"Parameter '{self.name}' must be a float")
49
+ elif self.type == "bool":
50
+ if isinstance(value, bool):
51
+ return value
52
+ if isinstance(value, str):
53
+ return value.lower() in ("true", "yes", "1", "y")
54
+ return bool(value)
55
+ else:
56
+ raise ValueError(f"Unknown parameter type: {self.type}")
57
+
58
+
59
+ class ShellToolDefinition(BaseModel):
60
+ """Definition of a shell-based tool."""
61
+
62
+ model_config = ConfigDict(
63
+ extra="forbid",
64
+ )
65
+
66
+ name: str
67
+ description: str
68
+ command: str # Template string with {param} placeholders
69
+ parameters: Dict[str, ShellToolParameter] = Field(default_factory=dict)
70
+ timeout: int = 30
71
+ safe_mode: bool = True # Use run_safe vs run
72
+ shell: bool = True # Execute via shell
73
+
74
+
75
+ def interpolate_command(command_template: str, params: Dict[str, Any]) -> str:
76
+ """Interpolate parameters into command template.
77
+
78
+ Args:
79
+ command_template: Command string with {param} placeholders
80
+ params: Dictionary of parameter values
81
+
82
+ Returns:
83
+ Interpolated command string
84
+
85
+ Example:
86
+ >>> interpolate_command("rg {pattern} {path}", {"pattern": "test", "path": "."})
87
+ 'rg test .'
88
+ """
89
+ # Handle boolean flags specially
90
+ interpolated = command_template
91
+
92
+ # First, handle any boolean parameters with flags
93
+ for key, value in params.items():
94
+ # If value is boolean and the key appears with a flag placeholder
95
+ if isinstance(value, bool):
96
+ # Replace {key} with flag or empty string
97
+ flag_value = params.get(f"_flag_{key}", "")
98
+ if value and flag_value:
99
+ interpolated = interpolated.replace(f"{{{key}}}", flag_value)
100
+ else:
101
+ interpolated = interpolated.replace(f"{{{key}}}", "")
102
+
103
+ # Then handle regular parameter substitution
104
+ try:
105
+ interpolated = interpolated.format(**params)
106
+ except KeyError as e:
107
+ raise ValueError(f"Missing required parameter in command template: {e}")
108
+
109
+ return interpolated.strip()
110
+
111
+
112
+ def create_shell_tool_function(definition: ShellToolDefinition):
113
+ """Create a Python function that executes a shell tool.
114
+
115
+ Args:
116
+ definition: Shell tool definition
117
+
118
+ Returns:
119
+ Callable function that can be registered as a tool
120
+ """
121
+ import inspect
122
+
123
+ def shell_tool_func(**kwargs) -> str:
124
+ """Dynamically generated shell tool function."""
125
+ # Validate and process parameters
126
+ processed_params = {}
127
+
128
+ for param_name, param_def in definition.parameters.items():
129
+ value = kwargs.get(param_name)
130
+ validated_value = param_def.validate(value)
131
+
132
+ # Handle boolean flags
133
+ if param_def.type == "bool" and param_def.flag:
134
+ if validated_value:
135
+ processed_params[f"_flag_{param_name}"] = param_def.flag
136
+ processed_params[param_name] = validated_value
137
+ else:
138
+ processed_params[param_name] = validated_value
139
+
140
+ # Interpolate command
141
+ try:
142
+ command = interpolate_command(definition.command, processed_params)
143
+ except Exception as e:
144
+ raise RuntimeError(f"Failed to build command: {e}")
145
+
146
+ # Execute command
147
+ return execute_shell_command(command, timeout=definition.timeout, shell=definition.shell)
148
+
149
+ # Set function metadata for tool registration
150
+ shell_tool_func.__name__ = definition.name
151
+ shell_tool_func.__doc__ = definition.description
152
+
153
+ # Build proper signature with actual parameters
154
+ # This is critical for @tool decorator to work correctly
155
+ params = []
156
+ annotations = {}
157
+
158
+ for param_name, param_def in definition.parameters.items():
159
+ # Map our types to Python types
160
+ if param_def.type == "str":
161
+ param_type = str
162
+ elif param_def.type == "int":
163
+ param_type = int
164
+ elif param_def.type == "bool":
165
+ param_type = bool
166
+ elif param_def.type == "float":
167
+ param_type = float
168
+ else:
169
+ param_type = str
170
+
171
+ annotations[param_name] = param_type
172
+
173
+ # Create Parameter with default if not required
174
+ if param_def.required:
175
+ param = inspect.Parameter(
176
+ param_name,
177
+ inspect.Parameter.KEYWORD_ONLY,
178
+ annotation=param_type,
179
+ )
180
+ else:
181
+ param = inspect.Parameter(
182
+ param_name,
183
+ inspect.Parameter.KEYWORD_ONLY,
184
+ default=param_def.default,
185
+ annotation=param_type,
186
+ )
187
+ params.append(param)
188
+
189
+ # Create new signature and assign it
190
+ new_sig = inspect.Signature(params)
191
+ shell_tool_func.__signature__ = new_sig
192
+ shell_tool_func.__annotations__ = annotations
193
+
194
+ return shell_tool_func
195
+
196
+
197
+ def register_shell_tool(definition: ShellToolDefinition) -> None:
198
+ """Register a shell tool definition as a tool.
199
+
200
+ Args:
201
+ definition: Shell tool definition to register
202
+ """
203
+ func = create_shell_tool_function(definition)
204
+ tool(func)
205
+
206
+
207
+ def register_shell_tools(definitions: List[ShellToolDefinition]) -> None:
208
+ """Register multiple shell tool definitions.
209
+
210
+ Args:
211
+ definitions: List of shell tool definitions to register
212
+ """
213
+ for definition in definitions:
214
+ register_shell_tool(definition)
tsugite/tools/tasks.py ADDED
@@ -0,0 +1,339 @@
1
+ """Task tracking tools for agents to manage work across execution steps."""
2
+
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field
8
+
9
+ from . import tool
10
+
11
+
12
+ class TaskStatus(Enum):
13
+ """Valid status values for tasks."""
14
+
15
+ PENDING = "pending"
16
+ IN_PROGRESS = "in_progress"
17
+ COMPLETED = "completed"
18
+ BLOCKED = "blocked"
19
+ CANCELLED = "cancelled"
20
+
21
+
22
+ class Task(BaseModel):
23
+ """Individual task with metadata."""
24
+
25
+ model_config = ConfigDict(
26
+ extra="forbid",
27
+ )
28
+
29
+ id: int
30
+ title: str
31
+ status: TaskStatus
32
+ parent_id: Optional[int] = None
33
+ optional: bool = False
34
+ created_at: datetime = Field(default_factory=datetime.now)
35
+ updated_at: datetime = Field(default_factory=datetime.now)
36
+ completed_at: Optional[datetime] = None
37
+
38
+
39
+ class TaskManager:
40
+ """Manages tasks during a single agent execution session."""
41
+
42
+ def __init__(self):
43
+ self.tasks: Dict[int, Task] = {}
44
+ self._next_id = 1
45
+
46
+ def add_task(
47
+ self,
48
+ title: str,
49
+ status: TaskStatus = TaskStatus.PENDING,
50
+ parent_id: Optional[int] = None,
51
+ optional: bool = False,
52
+ ) -> int:
53
+ """Add a new task and return its ID."""
54
+ task_id = self._next_id
55
+ self._next_id += 1
56
+
57
+ if parent_id is not None and parent_id not in self.tasks:
58
+ raise ValueError(f"Parent task {parent_id} does not exist")
59
+
60
+ task = Task(id=task_id, title=title, status=status, parent_id=parent_id, optional=optional)
61
+
62
+ self.tasks[task_id] = task
63
+ return task_id
64
+
65
+ def update_task(self, task_id: int, status: TaskStatus) -> None:
66
+ """Update a task's status."""
67
+ if task_id not in self.tasks:
68
+ raise ValueError(f"Task {task_id} does not exist")
69
+
70
+ task = self.tasks[task_id]
71
+ task.status = status
72
+ task.updated_at = datetime.now()
73
+
74
+ if status == TaskStatus.COMPLETED:
75
+ task.completed_at = datetime.now()
76
+
77
+ def get_task(self, task_id: int) -> Task:
78
+ """Get a specific task by ID."""
79
+ if task_id not in self.tasks:
80
+ raise ValueError(f"Task {task_id} does not exist")
81
+ return self.tasks[task_id]
82
+
83
+ def list_tasks(self, status: Optional[TaskStatus] = None, parent_id: Optional[int] = None) -> List[Task]:
84
+ """List tasks with optional filtering."""
85
+ tasks = list(self.tasks.values())
86
+
87
+ if status is not None:
88
+ tasks = [t for t in tasks if t.status == status]
89
+
90
+ if parent_id is not None:
91
+ tasks = [t for t in tasks if t.parent_id == parent_id]
92
+
93
+ # Sort by creation time
94
+ return sorted(tasks, key=lambda t: t.created_at)
95
+
96
+ def get_tasks_for_template(self) -> List[Dict[str, Any]]:
97
+ """Get all tasks as template-friendly dicts.
98
+
99
+ Returns same format as task_list() tool for consistency.
100
+ Useful for iterating over tasks in Jinja2 templates.
101
+
102
+ Returns:
103
+ List of task dictionaries with all fields
104
+ """
105
+ tasks = self.list_tasks()
106
+
107
+ return [
108
+ {
109
+ "id": task.id,
110
+ "title": task.title,
111
+ "status": task.status.value,
112
+ "parent_id": task.parent_id,
113
+ "optional": task.optional,
114
+ "created_at": task.created_at.isoformat(),
115
+ "updated_at": task.updated_at.isoformat(),
116
+ "completed_at": task.completed_at.isoformat() if task.completed_at else None,
117
+ }
118
+ for task in tasks
119
+ ]
120
+
121
+ def get_task_summary(self) -> str:
122
+ """Generate a formatted summary of all tasks for agent context."""
123
+ if not self.tasks:
124
+ return "## Current Tasks\nNo tasks yet."
125
+
126
+ total_tasks = len(self.tasks)
127
+ completed_tasks = len([t for t in self.tasks.values() if t.status == TaskStatus.COMPLETED])
128
+
129
+ summary = [f"## Current Tasks ({completed_tasks} completed / {total_tasks} total)\n"]
130
+
131
+ # Group tasks by status
132
+ status_groups = {
133
+ "Active Tasks": [TaskStatus.IN_PROGRESS, TaskStatus.PENDING],
134
+ "Blocked Tasks": [TaskStatus.BLOCKED],
135
+ "Completed Tasks": [TaskStatus.COMPLETED],
136
+ "Cancelled Tasks": [TaskStatus.CANCELLED],
137
+ }
138
+
139
+ status_icons = {
140
+ TaskStatus.PENDING: "⏸️",
141
+ TaskStatus.IN_PROGRESS: "⏳",
142
+ TaskStatus.COMPLETED: "βœ…",
143
+ TaskStatus.BLOCKED: "🚫",
144
+ TaskStatus.CANCELLED: "❌",
145
+ }
146
+
147
+ for group_name, statuses in status_groups.items():
148
+ group_tasks = [t for t in self.tasks.values() if t.status in statuses]
149
+ if not group_tasks:
150
+ continue
151
+
152
+ summary.append(f"### {group_name}")
153
+
154
+ # Show parent tasks first, then their subtasks indented
155
+ parent_tasks = [t for t in group_tasks if t.parent_id is None]
156
+ parent_tasks.sort(key=lambda t: t.id)
157
+
158
+ for parent in parent_tasks:
159
+ icon = status_icons[parent.status]
160
+ optional_marker = " ✨ (optional)" if parent.optional else ""
161
+ summary.append(f"[{parent.id}] {icon} {parent.title}{optional_marker}")
162
+
163
+ # Show subtasks indented
164
+ subtasks = [t for t in group_tasks if t.parent_id == parent.id]
165
+ subtasks.sort(key=lambda t: t.id)
166
+ for subtask in subtasks:
167
+ sub_icon = status_icons[subtask.status]
168
+ sub_optional = " ✨ (optional)" if subtask.optional else ""
169
+ summary.append(f" └─ [{subtask.id}] {sub_icon} {subtask.title}{sub_optional}")
170
+
171
+ # Show orphaned subtasks (parent not in this group)
172
+ orphaned = [
173
+ t for t in group_tasks if t.parent_id is not None and t.parent_id not in [p.id for p in parent_tasks]
174
+ ]
175
+ for orphan in orphaned:
176
+ icon = status_icons[orphan.status]
177
+ optional_marker = " ✨ (optional)" if orphan.optional else ""
178
+ summary.append(f"[{orphan.id}] {icon} {orphan.title}{optional_marker} (subtask of #{orphan.parent_id})")
179
+
180
+ summary.append("")
181
+
182
+ summary.append("Status: ⏸️ pending | ⏳ in_progress | βœ… completed | 🚫 blocked | ❌ cancelled")
183
+
184
+ return "\n".join(summary)
185
+
186
+
187
+ # Global task manager instance for the current agent session
188
+ _task_manager: Optional[TaskManager] = None
189
+
190
+
191
+ def get_task_manager() -> TaskManager:
192
+ """Get the current task manager instance."""
193
+ global _task_manager
194
+ if _task_manager is None:
195
+ _task_manager = TaskManager()
196
+ return _task_manager
197
+
198
+
199
+ def reset_task_manager() -> None:
200
+ """Reset the task manager (used at start of new agent session)."""
201
+ global _task_manager
202
+ _task_manager = TaskManager()
203
+
204
+
205
+ @tool
206
+ def task_add(title: str, status: str = "pending", parent_id: Optional[int] = None, optional: bool = False) -> int:
207
+ """Add a new task or subtask.
208
+
209
+ Args:
210
+ title: Description of the task
211
+ status: Task status (pending/in_progress/completed/blocked/cancelled)
212
+ parent_id: ID of parent task if this is a subtask (omit or use None for root tasks)
213
+ optional: Whether this task is optional (nice-to-have vs required)
214
+
215
+ Returns:
216
+ ID of the created task
217
+ """
218
+ try:
219
+ task_status = TaskStatus(status)
220
+ except ValueError:
221
+ valid_statuses = [s.value for s in TaskStatus]
222
+ raise ValueError(f"Invalid status '{status}'. Valid options: {valid_statuses}")
223
+
224
+ # Treat 0 as None (root task) for convenience
225
+ if parent_id == 0:
226
+ parent_id = None
227
+
228
+ manager = get_task_manager()
229
+ return manager.add_task(title, task_status, parent_id, optional)
230
+
231
+
232
+ @tool
233
+ def task_update(task_id: int, status: str) -> str:
234
+ """Update a task's status.
235
+
236
+ Args:
237
+ task_id: ID of the task to update
238
+ status: New status (pending/in_progress/completed/blocked/cancelled)
239
+
240
+ Returns:
241
+ Confirmation message with task details and new status
242
+ """
243
+ try:
244
+ task_status = TaskStatus(status)
245
+ except ValueError:
246
+ valid_statuses = [s.value for s in TaskStatus]
247
+ raise ValueError(f"Invalid status '{status}'. Valid options: {valid_statuses}")
248
+
249
+ manager = get_task_manager()
250
+ task = manager.get_task(task_id) # Get task before updating to include title in response
251
+ manager.update_task(task_id, task_status)
252
+
253
+ # Use status icons for visual clarity
254
+ status_icons = {
255
+ "pending": "⏸️",
256
+ "in_progress": "⏳",
257
+ "completed": "βœ…",
258
+ "blocked": "🚫",
259
+ "cancelled": "❌",
260
+ }
261
+ icon = status_icons.get(status, "")
262
+ return f"{icon} Updated task #{task_id}: '{task.title}' β†’ {status}"
263
+
264
+
265
+ @tool
266
+ def task_complete(task_id: int) -> str:
267
+ """Mark a task as completed (shortcut for task_update).
268
+
269
+ Args:
270
+ task_id: ID of the task to mark as completed
271
+
272
+ Returns:
273
+ Confirmation message with task details
274
+ """
275
+ manager = get_task_manager()
276
+ task = manager.get_task(task_id) # Get task before updating to include title in response
277
+ manager.update_task(task_id, TaskStatus.COMPLETED)
278
+ return f"βœ… Completed task #{task_id}: '{task.title}'"
279
+
280
+
281
+ @tool
282
+ def task_list(status: Optional[str] = None) -> List[Dict[str, Any]]:
283
+ """List tasks with optional status filtering.
284
+
285
+ Args:
286
+ status: Optional status filter (pending/in_progress/completed/blocked/cancelled)
287
+
288
+ Returns:
289
+ List of task dictionaries with id, title, status, parent_id
290
+ """
291
+ status_filter = None
292
+ if status is not None:
293
+ try:
294
+ status_filter = TaskStatus(status)
295
+ except ValueError:
296
+ valid_statuses = [s.value for s in TaskStatus]
297
+ raise ValueError(f"Invalid status '{status}'. Valid options: {valid_statuses}")
298
+
299
+ manager = get_task_manager()
300
+ tasks = manager.list_tasks(status_filter)
301
+
302
+ return [
303
+ {
304
+ "id": task.id,
305
+ "title": task.title,
306
+ "status": task.status.value,
307
+ "parent_id": task.parent_id,
308
+ "optional": task.optional,
309
+ "created_at": task.created_at.isoformat(),
310
+ "updated_at": task.updated_at.isoformat(),
311
+ "completed_at": task.completed_at.isoformat() if task.completed_at else None,
312
+ }
313
+ for task in tasks
314
+ ]
315
+
316
+
317
+ @tool
318
+ def task_get(task_id: int) -> Dict[str, Any]:
319
+ """Get details for a specific task.
320
+
321
+ Args:
322
+ task_id: ID of the task to retrieve
323
+
324
+ Returns:
325
+ Task dictionary with all details
326
+ """
327
+ manager = get_task_manager()
328
+ task = manager.get_task(task_id)
329
+
330
+ return {
331
+ "id": task.id,
332
+ "title": task.title,
333
+ "status": task.status.value,
334
+ "parent_id": task.parent_id,
335
+ "optional": task.optional,
336
+ "created_at": task.created_at.isoformat(),
337
+ "updated_at": task.updated_at.isoformat(),
338
+ "completed_at": task.completed_at.isoformat() if task.completed_at else None,
339
+ }
tsugite/tsugite.py ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env python3
2
+ """Tsugite CLI application - entrypoint."""
3
+
4
+ from tsugite.cli import app
5
+
6
+ if __name__ == "__main__":
7
+ app()
tsugite/ui/__init__.py ADDED
@@ -0,0 +1,46 @@
1
+ """UI system for controlling agent execution display.
2
+
3
+ This module provides a flexible UI system with multiple handlers:
4
+ - CustomUIHandler: Rich UI with panels, colors, and emojis (default)
5
+ - PlainUIHandler: Plain text output for copy-paste workflows
6
+ - LiveTemplateHandler: Live Display with Tree and interactive prompts
7
+ - TextualUIHandler: Textual TUI chat interface handler
8
+
9
+ Use the helper functions to create loggers:
10
+ - custom_agent_ui(): Context manager for rich UI
11
+ - create_plain_logger(): Plain text logger
12
+ - create_live_template_logger(): Live display with tree visualization
13
+ """
14
+
15
+ from tsugite.ui.base import CustomUIHandler, CustomUILogger, UIState
16
+ from tsugite.ui.chat import ChatManager, ChatTurn
17
+ from tsugite.ui.helpers import (
18
+ create_live_template_logger,
19
+ create_plain_logger,
20
+ custom_agent_ui,
21
+ )
22
+ from tsugite.ui.live_template import LiveTemplateHandler
23
+ from tsugite.ui.plain import PlainUIHandler
24
+ from tsugite.ui.textual_handler import TextualUIHandler
25
+
26
+ # Import textual_chat to make it accessible as attribute for tests
27
+ # Must be after other imports since textual_chat imports from tsugite.ui
28
+ from tsugite.ui import textual_chat # noqa: F401 # isort: skip
29
+
30
+ __all__ = [
31
+ # Core classes
32
+ "UIState",
33
+ "CustomUILogger",
34
+ # UI Handlers
35
+ "CustomUIHandler",
36
+ "PlainUIHandler",
37
+ "LiveTemplateHandler",
38
+ "TextualUIHandler",
39
+ # Chat functionality
40
+ "ChatManager",
41
+ "ChatTurn",
42
+ # Helper functions
43
+ "custom_agent_ui",
44
+ "create_plain_logger",
45
+ "create_live_template_logger",
46
+ ]