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.
- tsugite/__init__.py +6 -0
- tsugite/agent_composition.py +163 -0
- tsugite/agent_inheritance.py +479 -0
- tsugite/agent_preparation.py +236 -0
- tsugite/agent_runner/__init__.py +45 -0
- tsugite/agent_runner/helpers.py +106 -0
- tsugite/agent_runner/history_integration.py +248 -0
- tsugite/agent_runner/metrics.py +100 -0
- tsugite/agent_runner/runner.py +1879 -0
- tsugite/agent_runner/validation.py +70 -0
- tsugite/agent_utils.py +167 -0
- tsugite/attachments/__init__.py +65 -0
- tsugite/attachments/auto_context.py +199 -0
- tsugite/attachments/base.py +34 -0
- tsugite/attachments/file.py +51 -0
- tsugite/attachments/inline.py +31 -0
- tsugite/attachments/storage.py +178 -0
- tsugite/attachments/url.py +59 -0
- tsugite/attachments/youtube.py +101 -0
- tsugite/benchmark/__init__.py +62 -0
- tsugite/benchmark/config.py +183 -0
- tsugite/benchmark/core.py +292 -0
- tsugite/benchmark/discovery.py +377 -0
- tsugite/benchmark/evaluators.py +671 -0
- tsugite/benchmark/execution.py +657 -0
- tsugite/benchmark/metrics.py +204 -0
- tsugite/benchmark/reports.py +420 -0
- tsugite/benchmark/utils.py +288 -0
- tsugite/builtin_agents/chat-assistant.md +53 -0
- tsugite/builtin_agents/default.md +140 -0
- tsugite/builtin_agents.py +5 -0
- tsugite/cache.py +195 -0
- tsugite/cli/__init__.py +1042 -0
- tsugite/cli/agents.py +148 -0
- tsugite/cli/attachments.py +193 -0
- tsugite/cli/benchmark.py +663 -0
- tsugite/cli/cache.py +113 -0
- tsugite/cli/config.py +272 -0
- tsugite/cli/helpers.py +534 -0
- tsugite/cli/history.py +193 -0
- tsugite/cli/init.py +387 -0
- tsugite/cli/mcp.py +193 -0
- tsugite/cli/tools.py +419 -0
- tsugite/config.py +204 -0
- tsugite/console.py +48 -0
- tsugite/constants.py +21 -0
- tsugite/core/__init__.py +19 -0
- tsugite/core/agent.py +774 -0
- tsugite/core/executor.py +300 -0
- tsugite/core/memory.py +67 -0
- tsugite/core/tools.py +271 -0
- tsugite/docker_cli.py +270 -0
- tsugite/events/__init__.py +55 -0
- tsugite/events/base.py +46 -0
- tsugite/events/bus.py +62 -0
- tsugite/events/events.py +224 -0
- tsugite/exceptions.py +40 -0
- tsugite/history/__init__.py +29 -0
- tsugite/history/index.py +210 -0
- tsugite/history/models.py +106 -0
- tsugite/history/storage.py +157 -0
- tsugite/mcp_client.py +219 -0
- tsugite/mcp_config.py +174 -0
- tsugite/md_agents.py +751 -0
- tsugite/models.py +257 -0
- tsugite/renderer.py +151 -0
- tsugite/shell_tool_config.py +265 -0
- tsugite/templates/assistant.md +14 -0
- tsugite/tools/__init__.py +265 -0
- tsugite/tools/agents.py +312 -0
- tsugite/tools/edit_strategies.py +393 -0
- tsugite/tools/fs.py +329 -0
- tsugite/tools/http.py +239 -0
- tsugite/tools/interactive.py +430 -0
- tsugite/tools/shell.py +129 -0
- tsugite/tools/shell_tools.py +214 -0
- tsugite/tools/tasks.py +339 -0
- tsugite/tsugite.py +7 -0
- tsugite/ui/__init__.py +46 -0
- tsugite/ui/base.py +638 -0
- tsugite/ui/chat.py +265 -0
- tsugite/ui/chat.tcss +92 -0
- tsugite/ui/chat_history.py +286 -0
- tsugite/ui/helpers.py +102 -0
- tsugite/ui/jsonl.py +125 -0
- tsugite/ui/live_template.py +529 -0
- tsugite/ui/plain.py +419 -0
- tsugite/ui/textual_chat.py +642 -0
- tsugite/ui/textual_handler.py +225 -0
- tsugite/ui/widgets/__init__.py +6 -0
- tsugite/ui/widgets/base_scroll_log.py +27 -0
- tsugite/ui/widgets/message_list.py +121 -0
- tsugite/ui/widgets/thought_log.py +80 -0
- tsugite/ui_context.py +90 -0
- tsugite/utils.py +367 -0
- tsugite/xdg.py +104 -0
- tsugite_cli-0.3.3.dist-info/METADATA +325 -0
- tsugite_cli-0.3.3.dist-info/RECORD +101 -0
- tsugite_cli-0.3.3.dist-info/WHEEL +4 -0
- tsugite_cli-0.3.3.dist-info/entry_points.txt +5 -0
- 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
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
|
+
]
|