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
tsugite/cli/__init__.py
ADDED
|
@@ -0,0 +1,1042 @@
|
|
|
1
|
+
"""Tsugite CLI application - main entry point."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.traceback import install
|
|
11
|
+
|
|
12
|
+
from .agents import agents_app
|
|
13
|
+
from .attachments import attachments_app
|
|
14
|
+
from .benchmark import benchmark_command
|
|
15
|
+
from .cache import cache_app
|
|
16
|
+
from .config import config_app
|
|
17
|
+
from .helpers import (
|
|
18
|
+
assemble_prompt_with_attachments,
|
|
19
|
+
change_to_root_directory,
|
|
20
|
+
get_error_console,
|
|
21
|
+
get_logo,
|
|
22
|
+
inject_auto_context_if_enabled,
|
|
23
|
+
load_and_validate_agent,
|
|
24
|
+
print_plain_info,
|
|
25
|
+
)
|
|
26
|
+
from .history import history_app
|
|
27
|
+
from .init import init
|
|
28
|
+
from .mcp import mcp_app
|
|
29
|
+
from .tools import tools_app
|
|
30
|
+
|
|
31
|
+
# Chat history limit - keeps last N turns to balance context retention vs memory usage
|
|
32
|
+
DEFAULT_MAX_CHAT_HISTORY = 50
|
|
33
|
+
|
|
34
|
+
# Install rich traceback handler for better error messages
|
|
35
|
+
# This is after imports to satisfy linting, but still early enough to catch runtime errors
|
|
36
|
+
install(show_locals=False, width=None, word_wrap=True)
|
|
37
|
+
|
|
38
|
+
app = typer.Typer(
|
|
39
|
+
name="tsugite",
|
|
40
|
+
help="Micro-agent runner for task automation using markdown definitions",
|
|
41
|
+
no_args_is_help=True,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Global console for CLI messages (version, help, errors) - uses stdout
|
|
45
|
+
console = Console()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _build_docker_command(
|
|
49
|
+
args: List[str],
|
|
50
|
+
network: str,
|
|
51
|
+
keep: bool,
|
|
52
|
+
container: Optional[str],
|
|
53
|
+
model: Optional[str],
|
|
54
|
+
with_agents: Optional[str],
|
|
55
|
+
root: Optional[str],
|
|
56
|
+
history_dir: Optional[str],
|
|
57
|
+
ui: Optional[str],
|
|
58
|
+
debug: bool,
|
|
59
|
+
verbose: bool,
|
|
60
|
+
headless: bool,
|
|
61
|
+
plain: bool,
|
|
62
|
+
show_reasoning: bool,
|
|
63
|
+
no_color: bool,
|
|
64
|
+
final_only: bool,
|
|
65
|
+
log_json: bool,
|
|
66
|
+
non_interactive: bool,
|
|
67
|
+
trust_mcp_code: bool,
|
|
68
|
+
attachment: Optional[List[str]],
|
|
69
|
+
refresh_cache: bool,
|
|
70
|
+
) -> List[str]:
|
|
71
|
+
"""Build Docker wrapper command with all flags.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
args: Agent references and prompt
|
|
75
|
+
network: Docker network mode
|
|
76
|
+
keep: Keep container running flag
|
|
77
|
+
container: Existing container name
|
|
78
|
+
model: Model override
|
|
79
|
+
with_agents: Additional agents
|
|
80
|
+
root: Working directory
|
|
81
|
+
history_dir: History directory
|
|
82
|
+
ui: UI mode
|
|
83
|
+
debug: Debug flag
|
|
84
|
+
verbose: Verbose flag
|
|
85
|
+
headless: Headless flag
|
|
86
|
+
plain: Plain output flag
|
|
87
|
+
show_reasoning: Show reasoning flag
|
|
88
|
+
no_color: No color flag
|
|
89
|
+
final_only: Final only flag
|
|
90
|
+
log_json: JSON logging flag
|
|
91
|
+
non_interactive: Non-interactive flag
|
|
92
|
+
trust_mcp_code: Trust MCP code flag
|
|
93
|
+
attachment: Attachment list
|
|
94
|
+
refresh_cache: Refresh cache flag
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Complete command list for subprocess execution
|
|
98
|
+
"""
|
|
99
|
+
cmd = ["tsugite-docker"]
|
|
100
|
+
|
|
101
|
+
if network != "host":
|
|
102
|
+
cmd.extend(["--network", network])
|
|
103
|
+
if keep:
|
|
104
|
+
cmd.append("--keep")
|
|
105
|
+
if container:
|
|
106
|
+
cmd.extend(["--container", container])
|
|
107
|
+
|
|
108
|
+
cmd.append("run")
|
|
109
|
+
|
|
110
|
+
cmd.extend(args)
|
|
111
|
+
|
|
112
|
+
if model:
|
|
113
|
+
cmd.extend(["--model", model])
|
|
114
|
+
if with_agents:
|
|
115
|
+
cmd.extend(["--with-agents", with_agents])
|
|
116
|
+
if root:
|
|
117
|
+
cmd.extend(["--root", str(root)])
|
|
118
|
+
if history_dir:
|
|
119
|
+
cmd.extend(["--history-dir", str(history_dir)])
|
|
120
|
+
if ui:
|
|
121
|
+
cmd.extend(["--ui", ui])
|
|
122
|
+
if debug:
|
|
123
|
+
cmd.append("--debug")
|
|
124
|
+
if verbose:
|
|
125
|
+
cmd.append("--verbose")
|
|
126
|
+
if headless:
|
|
127
|
+
cmd.append("--headless")
|
|
128
|
+
if plain:
|
|
129
|
+
cmd.append("--plain")
|
|
130
|
+
if show_reasoning:
|
|
131
|
+
cmd.append("--show-reasoning")
|
|
132
|
+
if no_color:
|
|
133
|
+
cmd.append("--no-color")
|
|
134
|
+
if final_only:
|
|
135
|
+
cmd.append("--final-only")
|
|
136
|
+
if log_json:
|
|
137
|
+
cmd.append("--log-json")
|
|
138
|
+
if non_interactive:
|
|
139
|
+
cmd.append("--non-interactive")
|
|
140
|
+
if trust_mcp_code:
|
|
141
|
+
cmd.append("--trust-mcp-code")
|
|
142
|
+
if attachment:
|
|
143
|
+
for att in attachment:
|
|
144
|
+
cmd.extend(["--attachment", att])
|
|
145
|
+
if refresh_cache:
|
|
146
|
+
cmd.append("--refresh-cache")
|
|
147
|
+
|
|
148
|
+
return cmd
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _resolve_ui_mode(ui: Optional[str], plain: bool, headless: bool, console: Console) -> tuple[bool, bool, bool]:
|
|
152
|
+
"""Resolve UI mode flag to individual UI control flags.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
ui: UI mode string (plain, headless, live)
|
|
156
|
+
plain: Plain output flag
|
|
157
|
+
headless: Headless mode flag
|
|
158
|
+
console: Console for error output
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Tuple of (plain, headless, live_ui) flags
|
|
162
|
+
|
|
163
|
+
Raises:
|
|
164
|
+
typer.Exit: If invalid UI mode or conflicting flags
|
|
165
|
+
"""
|
|
166
|
+
live_ui = False
|
|
167
|
+
|
|
168
|
+
if not ui:
|
|
169
|
+
return plain, headless, live_ui
|
|
170
|
+
|
|
171
|
+
if any([plain, headless]):
|
|
172
|
+
console.print("[red]Error: --ui cannot be used with --plain or --headless[/red]")
|
|
173
|
+
raise typer.Exit(1)
|
|
174
|
+
|
|
175
|
+
ui_modes = {
|
|
176
|
+
"plain": {"plain": True},
|
|
177
|
+
"headless": {"headless": True},
|
|
178
|
+
"live": {"live_ui": True},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
ui_lower = ui.lower()
|
|
182
|
+
if ui_lower not in ui_modes:
|
|
183
|
+
console.print(f"[red]Error: Invalid UI mode '{ui}'. Choose from: {', '.join(ui_modes.keys())}[/red]")
|
|
184
|
+
raise typer.Exit(1)
|
|
185
|
+
|
|
186
|
+
mode_settings = ui_modes[ui_lower]
|
|
187
|
+
plain = mode_settings.get("plain", plain)
|
|
188
|
+
headless = mode_settings.get("headless", headless)
|
|
189
|
+
live_ui = mode_settings.get("live_ui", live_ui)
|
|
190
|
+
|
|
191
|
+
return plain, headless, live_ui
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _build_executor_kwargs(
|
|
195
|
+
agent_file: Path,
|
|
196
|
+
prompt: str,
|
|
197
|
+
model: Optional[str],
|
|
198
|
+
debug: bool,
|
|
199
|
+
custom_logger: Any,
|
|
200
|
+
trust_mcp_code: bool,
|
|
201
|
+
delegation_agents: Any,
|
|
202
|
+
stream: bool,
|
|
203
|
+
continue_conversation_id: Optional[str],
|
|
204
|
+
resolved_attachments: List[Tuple[str, str]],
|
|
205
|
+
should_save_history: bool,
|
|
206
|
+
executor: Any,
|
|
207
|
+
) -> Dict[str, Any]:
|
|
208
|
+
"""Build executor kwargs dict for run_agent/run_multistep_agent.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
agent_file: Path to agent file
|
|
212
|
+
prompt: User prompt
|
|
213
|
+
model: Model override
|
|
214
|
+
debug: Debug flag
|
|
215
|
+
custom_logger: Logger instance
|
|
216
|
+
trust_mcp_code: Trust MCP code flag
|
|
217
|
+
delegation_agents: Delegation agents
|
|
218
|
+
stream: Stream flag
|
|
219
|
+
continue_conversation_id: Continuation conversation ID
|
|
220
|
+
resolved_attachments: Resolved attachments
|
|
221
|
+
should_save_history: Whether to save history
|
|
222
|
+
executor: Executor function (run_agent or run_multistep_agent)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
Dict of executor kwargs
|
|
226
|
+
"""
|
|
227
|
+
from tsugite.agent_runner import run_agent
|
|
228
|
+
|
|
229
|
+
kwargs = {
|
|
230
|
+
"agent_path": agent_file,
|
|
231
|
+
"prompt": prompt,
|
|
232
|
+
"model_override": model,
|
|
233
|
+
"debug": debug,
|
|
234
|
+
"custom_logger": custom_logger,
|
|
235
|
+
"trust_mcp_code": trust_mcp_code,
|
|
236
|
+
"delegation_agents": delegation_agents,
|
|
237
|
+
"stream": stream,
|
|
238
|
+
"continue_conversation_id": continue_conversation_id,
|
|
239
|
+
"attachments": resolved_attachments,
|
|
240
|
+
}
|
|
241
|
+
if should_save_history and executor == run_agent:
|
|
242
|
+
kwargs["return_token_usage"] = True
|
|
243
|
+
return kwargs
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _handle_docker_execution(
|
|
247
|
+
args: List[str],
|
|
248
|
+
network: str,
|
|
249
|
+
keep: bool,
|
|
250
|
+
container: Optional[str],
|
|
251
|
+
model: Optional[str],
|
|
252
|
+
with_agents: Optional[str],
|
|
253
|
+
root: Optional[str],
|
|
254
|
+
history_dir: Optional[str],
|
|
255
|
+
ui: Optional[str],
|
|
256
|
+
debug: bool,
|
|
257
|
+
verbose: bool,
|
|
258
|
+
headless: bool,
|
|
259
|
+
plain: bool,
|
|
260
|
+
show_reasoning: bool,
|
|
261
|
+
no_color: bool,
|
|
262
|
+
final_only: bool,
|
|
263
|
+
log_json: bool,
|
|
264
|
+
non_interactive: bool,
|
|
265
|
+
trust_mcp_code: bool,
|
|
266
|
+
attachment: Optional[List[str]],
|
|
267
|
+
refresh_cache: bool,
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Handle Docker container execution and exit.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
All run() command parameters
|
|
273
|
+
|
|
274
|
+
Raises:
|
|
275
|
+
typer.Exit: Always exits after Docker execution
|
|
276
|
+
"""
|
|
277
|
+
import shutil
|
|
278
|
+
import subprocess
|
|
279
|
+
|
|
280
|
+
wrapper_path = shutil.which("tsugite-docker")
|
|
281
|
+
if not wrapper_path:
|
|
282
|
+
console.print("[red]Error: tsugite-docker wrapper not found in PATH[/red]")
|
|
283
|
+
console.print("[yellow]Install it by adding tsugite/bin/ to your PATH[/yellow]")
|
|
284
|
+
console.print("[dim]See bin/README.md for installation instructions[/dim]")
|
|
285
|
+
raise typer.Exit(1)
|
|
286
|
+
|
|
287
|
+
cmd = _build_docker_command(
|
|
288
|
+
args,
|
|
289
|
+
network,
|
|
290
|
+
keep,
|
|
291
|
+
container,
|
|
292
|
+
model,
|
|
293
|
+
with_agents,
|
|
294
|
+
root,
|
|
295
|
+
history_dir,
|
|
296
|
+
ui,
|
|
297
|
+
debug,
|
|
298
|
+
verbose,
|
|
299
|
+
headless,
|
|
300
|
+
plain,
|
|
301
|
+
show_reasoning,
|
|
302
|
+
no_color,
|
|
303
|
+
final_only,
|
|
304
|
+
log_json,
|
|
305
|
+
non_interactive,
|
|
306
|
+
trust_mcp_code,
|
|
307
|
+
attachment,
|
|
308
|
+
refresh_cache,
|
|
309
|
+
)
|
|
310
|
+
result = subprocess.run(cmd, check=False)
|
|
311
|
+
raise typer.Exit(result.returncode)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _resolve_conversation_continuation(continue_conversation: bool, conversation_id: Optional[str]) -> Optional[str]:
|
|
315
|
+
"""Resolve which conversation to continue.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
continue_conversation: Whether to continue a conversation
|
|
319
|
+
conversation_id: Specific conversation ID or None for latest
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Conversation ID to continue, or None if not continuing
|
|
323
|
+
|
|
324
|
+
Raises:
|
|
325
|
+
typer.Exit: If no conversations found
|
|
326
|
+
"""
|
|
327
|
+
if not continue_conversation:
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
from tsugite.ui.chat_history import get_latest_conversation
|
|
331
|
+
|
|
332
|
+
if conversation_id:
|
|
333
|
+
console.print(f"[cyan]Continuing conversation: {conversation_id}[/cyan]")
|
|
334
|
+
return conversation_id
|
|
335
|
+
|
|
336
|
+
continue_conversation_id = get_latest_conversation()
|
|
337
|
+
if not continue_conversation_id:
|
|
338
|
+
console.print("[red]No conversations found to resume[/red]")
|
|
339
|
+
raise typer.Exit(1)
|
|
340
|
+
|
|
341
|
+
console.print(f"[cyan]Continuing latest conversation: {continue_conversation_id}[/cyan]")
|
|
342
|
+
return continue_conversation_id
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@app.command()
|
|
346
|
+
def run(
|
|
347
|
+
args: List[str] = typer.Argument(
|
|
348
|
+
..., help="Agent(s) and optional prompt (e.g., +assistant 'task' or +a +b +c 'task' or +a create ticket)"
|
|
349
|
+
),
|
|
350
|
+
root: Optional[str] = typer.Option(None, "--root", help="Working directory"),
|
|
351
|
+
with_agents: Optional[str] = typer.Option(
|
|
352
|
+
None, "--with-agents", help="Additional agents (comma or space separated)"
|
|
353
|
+
),
|
|
354
|
+
model: Optional[str] = typer.Option(None, "--model", help="Override agent model"),
|
|
355
|
+
ui: Optional[str] = typer.Option(None, "--ui", help="UI mode: plain, headless, or live (default: minimal)"),
|
|
356
|
+
non_interactive: bool = typer.Option(False, "--non-interactive", help="Run without interactive prompts"),
|
|
357
|
+
history_dir: Optional[str] = typer.Option(None, "--history-dir", help="Directory to store history files"),
|
|
358
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable ANSI colors"),
|
|
359
|
+
log_json: bool = typer.Option(False, "--log-json", help="Machine-readable output"),
|
|
360
|
+
debug: bool = typer.Option(False, "--debug", help="Show rendered prompt before execution"),
|
|
361
|
+
final_only: bool = typer.Option(
|
|
362
|
+
False, "--final-only", "--quiet", help="Output only the final answer (suppress progress)"
|
|
363
|
+
),
|
|
364
|
+
show_reasoning: bool = typer.Option(
|
|
365
|
+
True, "--show-reasoning/--no-show-reasoning", help="Show LLM reasoning messages (default: enabled)"
|
|
366
|
+
),
|
|
367
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show all execution details"),
|
|
368
|
+
headless: bool = typer.Option(
|
|
369
|
+
False, "--headless", help="Headless mode for CI/scripts: result to stdout, optional progress to stderr"
|
|
370
|
+
),
|
|
371
|
+
plain: bool = typer.Option(False, "--plain", help="Plain output without panels/boxes (copy-paste friendly)"),
|
|
372
|
+
stream: bool = typer.Option(False, "--stream", help="Stream LLM responses in real-time"),
|
|
373
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Show execution plan without running agent"),
|
|
374
|
+
trust_mcp_code: bool = typer.Option(False, "--trust-mcp-code", help="Trust remote code from MCP servers"),
|
|
375
|
+
attachment: Optional[List[str]] = typer.Option(
|
|
376
|
+
None, "-f", "--attachment", help="Attachment(s) to include (repeatable)"
|
|
377
|
+
),
|
|
378
|
+
refresh_cache: bool = typer.Option(False, "--refresh-cache", help="Force refresh cached attachment content"),
|
|
379
|
+
auto_context: Optional[bool] = typer.Option(
|
|
380
|
+
None,
|
|
381
|
+
"--auto-context/--no-auto-context",
|
|
382
|
+
help="Enable/disable auto-context attachments (overrides config/agent)",
|
|
383
|
+
),
|
|
384
|
+
docker: bool = typer.Option(False, "--docker", help="Run in Docker container (delegates to tsugite-docker)"),
|
|
385
|
+
keep: bool = typer.Option(False, "--keep", help="Keep Docker container running (use with --docker)"),
|
|
386
|
+
container: Optional[str] = typer.Option(None, "--container", help="Use existing Docker container"),
|
|
387
|
+
network: str = typer.Option("host", "--network", help="Docker network mode (use with --docker)"),
|
|
388
|
+
no_history: bool = typer.Option(False, "--no-history", help="Disable conversation history persistence"),
|
|
389
|
+
continue_conversation: bool = typer.Option(False, "--continue", "-c", help="Continue previous conversation"),
|
|
390
|
+
conversation_id: Optional[str] = typer.Option(
|
|
391
|
+
None, "--conversation-id", help="Specific conversation ID to continue (use with --continue)"
|
|
392
|
+
),
|
|
393
|
+
subagent_mode: bool = typer.Option(
|
|
394
|
+
False, "--subagent-mode", help="Run as subagent: read JSON from stdin, emit JSONL to stdout"
|
|
395
|
+
),
|
|
396
|
+
):
|
|
397
|
+
"""Run an agent with the given prompt.
|
|
398
|
+
|
|
399
|
+
Examples:
|
|
400
|
+
tsu run agent.md "prompt"
|
|
401
|
+
tsu run +assistant "prompt"
|
|
402
|
+
tsu run +assistant +jira +coder "prompt"
|
|
403
|
+
tsu run +assistant create a ticket for bug 123
|
|
404
|
+
tsu run +assistant --with-agents "jira,coder" "prompt"
|
|
405
|
+
tsu run --continue "prompt" # Continue latest conversation (auto-detect agent)
|
|
406
|
+
tsu run +assistant --continue "prompt" # Continue latest with specific agent
|
|
407
|
+
tsu run --continue --conversation-id CONV_ID "prompt" # Continue specific conversation
|
|
408
|
+
"""
|
|
409
|
+
# Lazy imports - only load heavy dependencies when actually running agents
|
|
410
|
+
from tsugite.agent_runner import get_agent_info, run_agent
|
|
411
|
+
from tsugite.md_agents import validate_agent_execution
|
|
412
|
+
from tsugite.ui import create_live_template_logger, create_plain_logger, custom_agent_ui
|
|
413
|
+
from tsugite.utils import should_use_plain_output
|
|
414
|
+
|
|
415
|
+
if history_dir:
|
|
416
|
+
Path(history_dir).mkdir(parents=True, exist_ok=True)
|
|
417
|
+
|
|
418
|
+
if no_color:
|
|
419
|
+
console.no_color = True
|
|
420
|
+
|
|
421
|
+
# Resolve UI mode to individual flags
|
|
422
|
+
plain, headless, live_ui = _resolve_ui_mode(ui, plain, headless, console)
|
|
423
|
+
|
|
424
|
+
# Handle subagent mode - override incompatible settings
|
|
425
|
+
if subagent_mode:
|
|
426
|
+
import os
|
|
427
|
+
|
|
428
|
+
# Validate no conflicting flags
|
|
429
|
+
if plain or headless or live_ui:
|
|
430
|
+
console.print("[red]Error: --subagent-mode cannot be combined with --plain, --headless, or --live[/red]")
|
|
431
|
+
raise typer.Exit(1)
|
|
432
|
+
|
|
433
|
+
non_interactive = True
|
|
434
|
+
no_history = True
|
|
435
|
+
os.environ["TSUGITE_SUBAGENT_MODE"] = "1"
|
|
436
|
+
|
|
437
|
+
if docker or container:
|
|
438
|
+
_handle_docker_execution(
|
|
439
|
+
args,
|
|
440
|
+
network,
|
|
441
|
+
keep,
|
|
442
|
+
container,
|
|
443
|
+
model,
|
|
444
|
+
with_agents,
|
|
445
|
+
root,
|
|
446
|
+
history_dir,
|
|
447
|
+
ui,
|
|
448
|
+
debug,
|
|
449
|
+
verbose,
|
|
450
|
+
headless,
|
|
451
|
+
plain,
|
|
452
|
+
show_reasoning,
|
|
453
|
+
no_color,
|
|
454
|
+
final_only,
|
|
455
|
+
log_json,
|
|
456
|
+
non_interactive,
|
|
457
|
+
trust_mcp_code,
|
|
458
|
+
attachment,
|
|
459
|
+
refresh_cache,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
# Handle conversation continuation - check before parsing args
|
|
463
|
+
continue_conversation_id = _resolve_conversation_continuation(continue_conversation, conversation_id)
|
|
464
|
+
|
|
465
|
+
# Parse CLI arguments into agents and prompt (allow empty agents when continuing)
|
|
466
|
+
try:
|
|
467
|
+
from tsugite.cli.helpers import parse_cli_arguments
|
|
468
|
+
|
|
469
|
+
agent_refs, prompt, stdin_attachment = parse_cli_arguments(
|
|
470
|
+
args, allow_empty_agents=continue_conversation, check_stdin=not continue_conversation
|
|
471
|
+
)
|
|
472
|
+
except ValueError as e:
|
|
473
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
474
|
+
raise typer.Exit(1)
|
|
475
|
+
|
|
476
|
+
from tsugite.agent_composition import parse_agent_references
|
|
477
|
+
|
|
478
|
+
with change_to_root_directory(root, console):
|
|
479
|
+
try:
|
|
480
|
+
base_dir = Path.cwd()
|
|
481
|
+
|
|
482
|
+
if not agent_refs and continue_conversation:
|
|
483
|
+
from tsugite.history import get_conversation_metadata
|
|
484
|
+
|
|
485
|
+
metadata = get_conversation_metadata(continue_conversation_id)
|
|
486
|
+
if not metadata:
|
|
487
|
+
console.print(f"[red]Could not load metadata for conversation: {continue_conversation_id}[/red]")
|
|
488
|
+
raise typer.Exit(1)
|
|
489
|
+
|
|
490
|
+
agent_name = metadata.agent
|
|
491
|
+
console.print(f"[cyan]Auto-detected agent from conversation: {agent_name}[/cyan]")
|
|
492
|
+
agent_refs = [f"+{agent_name}"]
|
|
493
|
+
|
|
494
|
+
primary_agent_path, delegation_agents = parse_agent_references(agent_refs, with_agents, base_dir)
|
|
495
|
+
|
|
496
|
+
if not primary_agent_path.exists():
|
|
497
|
+
console.print(f"[red]Agent file not found: {primary_agent_path}[/red]")
|
|
498
|
+
raise typer.Exit(1)
|
|
499
|
+
|
|
500
|
+
if primary_agent_path.suffix != ".md":
|
|
501
|
+
console.print(f"[red]Agent file must be a .md file: {primary_agent_path}[/red]")
|
|
502
|
+
raise typer.Exit(1)
|
|
503
|
+
|
|
504
|
+
agent_file = primary_agent_path.resolve()
|
|
505
|
+
|
|
506
|
+
except ValueError as e:
|
|
507
|
+
console.print(f"[red]Error: {e}[/red]")
|
|
508
|
+
raise typer.Exit(1)
|
|
509
|
+
|
|
510
|
+
use_plain_output = plain or should_use_plain_output()
|
|
511
|
+
|
|
512
|
+
stderr_console = Console(file=sys.stderr, no_color=no_color)
|
|
513
|
+
|
|
514
|
+
agent_info = get_agent_info(agent_file)
|
|
515
|
+
instruction_label = "runtime + agent" if agent_info.get("instructions") else "runtime default"
|
|
516
|
+
|
|
517
|
+
agent_attachments = inject_auto_context_if_enabled(
|
|
518
|
+
agent_info.get("attachments"),
|
|
519
|
+
agent_info.get("auto_context"),
|
|
520
|
+
cli_override=auto_context,
|
|
521
|
+
)
|
|
522
|
+
|
|
523
|
+
prompt, resolved_attachments = assemble_prompt_with_attachments(
|
|
524
|
+
prompt=prompt,
|
|
525
|
+
agent_attachments=agent_attachments,
|
|
526
|
+
cli_attachments=attachment,
|
|
527
|
+
base_dir=base_dir,
|
|
528
|
+
refresh_cache=refresh_cache,
|
|
529
|
+
console=console,
|
|
530
|
+
stdin_attachment=stdin_attachment,
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
if not headless:
|
|
534
|
+
if not use_plain_output:
|
|
535
|
+
stderr_console.print(get_logo(stderr_console), style="cyan")
|
|
536
|
+
stderr_console.print()
|
|
537
|
+
|
|
538
|
+
info_items = {
|
|
539
|
+
"Agent": agent_file.name,
|
|
540
|
+
"Task": prompt,
|
|
541
|
+
"Directory": str(Path.cwd()),
|
|
542
|
+
"Model": model or agent_info.get("model", "unknown"),
|
|
543
|
+
"Instructions": instruction_label,
|
|
544
|
+
"Tools": ", ".join(agent_info.get("tools", [])),
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
if agent_attachments:
|
|
548
|
+
info_items["Agent Attachments"] = ", ".join(agent_attachments)
|
|
549
|
+
|
|
550
|
+
if attachment:
|
|
551
|
+
info_items["CLI Attachments"] = ", ".join(attachment)
|
|
552
|
+
|
|
553
|
+
if resolved_attachments:
|
|
554
|
+
info_items["Attachments"] = f"{len(resolved_attachments)} file(s)"
|
|
555
|
+
|
|
556
|
+
if use_plain_output:
|
|
557
|
+
print_plain_info(stderr_console, "Tsugite Agent Runner", info_items, style="cyan")
|
|
558
|
+
|
|
559
|
+
is_valid, error_msg = validate_agent_execution(agent_file)
|
|
560
|
+
if not is_valid:
|
|
561
|
+
get_error_console(headless, console).print(f"[red]Agent validation failed: {error_msg}[/red]")
|
|
562
|
+
raise typer.Exit(1)
|
|
563
|
+
|
|
564
|
+
from tsugite.agent_runner import preview_multistep_agent, run_multistep_agent
|
|
565
|
+
from tsugite.md_agents import has_step_directives
|
|
566
|
+
|
|
567
|
+
agent_text = agent_file.read_text()
|
|
568
|
+
is_multistep = has_step_directives(agent_text)
|
|
569
|
+
|
|
570
|
+
if dry_run:
|
|
571
|
+
if is_multistep:
|
|
572
|
+
preview_multistep_agent(
|
|
573
|
+
agent_path=agent_file,
|
|
574
|
+
prompt=prompt,
|
|
575
|
+
console=console,
|
|
576
|
+
)
|
|
577
|
+
else:
|
|
578
|
+
console.print("[yellow]Dry-run mode is for multi-step agents only.[/yellow]")
|
|
579
|
+
console.print("[dim]This is a single-step agent. Use --debug to see the rendered prompt.[/dim]")
|
|
580
|
+
return
|
|
581
|
+
|
|
582
|
+
executor = run_multistep_agent if is_multistep else run_agent
|
|
583
|
+
|
|
584
|
+
should_save_history = not no_history
|
|
585
|
+
|
|
586
|
+
if not headless and not final_only:
|
|
587
|
+
execution_type = "multi-step agent" if is_multistep else "agent"
|
|
588
|
+
stderr_console.print()
|
|
589
|
+
stderr_console.rule(f"[bold cyan]🚀 Starting {execution_type.title()} Execution[/bold cyan]")
|
|
590
|
+
stderr_console.print()
|
|
591
|
+
|
|
592
|
+
try:
|
|
593
|
+
# Choose execution mode based on flags
|
|
594
|
+
if headless or final_only:
|
|
595
|
+
# Headless/final-only mode: stderr for progress (if verbose and not final_only), stdout for result
|
|
596
|
+
stderr_console = get_error_console(True, console)
|
|
597
|
+
|
|
598
|
+
# In final_only mode, suppress all progress; in headless, respect verbose flag
|
|
599
|
+
show_progress_items = verbose and not final_only
|
|
600
|
+
|
|
601
|
+
with custom_agent_ui(
|
|
602
|
+
console=stderr_console,
|
|
603
|
+
show_code=show_progress_items,
|
|
604
|
+
show_observations=show_progress_items,
|
|
605
|
+
show_progress=False,
|
|
606
|
+
show_llm_messages=show_progress_items,
|
|
607
|
+
show_execution_results=show_progress_items,
|
|
608
|
+
show_execution_logs=show_progress_items,
|
|
609
|
+
show_panels=False,
|
|
610
|
+
show_debug_messages=verbose,
|
|
611
|
+
) as custom_logger:
|
|
612
|
+
executor_kwargs = _build_executor_kwargs(
|
|
613
|
+
agent_file,
|
|
614
|
+
prompt,
|
|
615
|
+
model,
|
|
616
|
+
debug,
|
|
617
|
+
custom_logger,
|
|
618
|
+
trust_mcp_code,
|
|
619
|
+
delegation_agents,
|
|
620
|
+
stream,
|
|
621
|
+
continue_conversation_id,
|
|
622
|
+
resolved_attachments,
|
|
623
|
+
should_save_history,
|
|
624
|
+
executor,
|
|
625
|
+
)
|
|
626
|
+
result = executor(**executor_kwargs)
|
|
627
|
+
elif live_ui:
|
|
628
|
+
custom_logger = create_live_template_logger(interactive=not non_interactive)
|
|
629
|
+
with custom_logger.ui_handler.progress_context():
|
|
630
|
+
executor_kwargs = _build_executor_kwargs(
|
|
631
|
+
agent_file,
|
|
632
|
+
prompt,
|
|
633
|
+
model,
|
|
634
|
+
debug,
|
|
635
|
+
custom_logger,
|
|
636
|
+
trust_mcp_code,
|
|
637
|
+
delegation_agents,
|
|
638
|
+
stream,
|
|
639
|
+
continue_conversation_id,
|
|
640
|
+
resolved_attachments,
|
|
641
|
+
should_save_history,
|
|
642
|
+
executor,
|
|
643
|
+
)
|
|
644
|
+
result = executor(**executor_kwargs)
|
|
645
|
+
else:
|
|
646
|
+
if use_plain_output:
|
|
647
|
+
custom_logger = create_plain_logger()
|
|
648
|
+
with custom_logger.ui_handler.progress_context():
|
|
649
|
+
executor_kwargs = _build_executor_kwargs(
|
|
650
|
+
agent_file,
|
|
651
|
+
prompt,
|
|
652
|
+
model,
|
|
653
|
+
debug,
|
|
654
|
+
custom_logger,
|
|
655
|
+
trust_mcp_code,
|
|
656
|
+
delegation_agents,
|
|
657
|
+
stream,
|
|
658
|
+
continue_conversation_id,
|
|
659
|
+
resolved_attachments,
|
|
660
|
+
should_save_history,
|
|
661
|
+
executor,
|
|
662
|
+
)
|
|
663
|
+
result = executor(**executor_kwargs)
|
|
664
|
+
else:
|
|
665
|
+
default_console = Console(file=sys.stderr, force_terminal=True, no_color=no_color)
|
|
666
|
+
with custom_agent_ui(
|
|
667
|
+
console=default_console,
|
|
668
|
+
show_code=not non_interactive,
|
|
669
|
+
show_observations=not non_interactive,
|
|
670
|
+
show_progress=not no_color,
|
|
671
|
+
show_llm_messages=show_reasoning,
|
|
672
|
+
show_execution_results=True,
|
|
673
|
+
show_execution_logs=verbose,
|
|
674
|
+
show_panels=False,
|
|
675
|
+
show_debug_messages=verbose,
|
|
676
|
+
) as custom_logger:
|
|
677
|
+
executor_kwargs = _build_executor_kwargs(
|
|
678
|
+
agent_file,
|
|
679
|
+
prompt,
|
|
680
|
+
model,
|
|
681
|
+
debug,
|
|
682
|
+
custom_logger,
|
|
683
|
+
trust_mcp_code,
|
|
684
|
+
delegation_agents,
|
|
685
|
+
stream,
|
|
686
|
+
continue_conversation_id,
|
|
687
|
+
resolved_attachments,
|
|
688
|
+
should_save_history,
|
|
689
|
+
executor,
|
|
690
|
+
)
|
|
691
|
+
result = executor(**executor_kwargs)
|
|
692
|
+
|
|
693
|
+
# Unpack result if history was enabled (run_agent returns tuple with metadata)
|
|
694
|
+
if should_save_history and executor == run_agent and isinstance(result, tuple):
|
|
695
|
+
result_str, token_count, cost, step_count, execution_steps, system_prompt, attachments = result
|
|
696
|
+
else:
|
|
697
|
+
result_str = result if not isinstance(result, tuple) else result[0]
|
|
698
|
+
token_count = None
|
|
699
|
+
cost = None
|
|
700
|
+
execution_steps = None
|
|
701
|
+
system_prompt = None
|
|
702
|
+
attachments = None
|
|
703
|
+
|
|
704
|
+
# Save to history (unless disabled)
|
|
705
|
+
if should_save_history:
|
|
706
|
+
try:
|
|
707
|
+
from tsugite.agent_runner.history_integration import save_run_to_history
|
|
708
|
+
|
|
709
|
+
agent_info = get_agent_info(agent_file)
|
|
710
|
+
save_run_to_history(
|
|
711
|
+
agent_path=agent_file,
|
|
712
|
+
agent_name=agent_info["name"],
|
|
713
|
+
prompt=prompt,
|
|
714
|
+
result=result_str,
|
|
715
|
+
model=model or agent_info.get("model", "default"),
|
|
716
|
+
token_count=token_count,
|
|
717
|
+
cost=cost,
|
|
718
|
+
execution_steps=execution_steps,
|
|
719
|
+
continue_conversation_id=continue_conversation_id,
|
|
720
|
+
system_prompt=system_prompt,
|
|
721
|
+
attachments=attachments,
|
|
722
|
+
)
|
|
723
|
+
except Exception as e:
|
|
724
|
+
# Don't fail the run if history save fails
|
|
725
|
+
print(f"Warning: Failed to save to history: {e}", file=sys.stderr)
|
|
726
|
+
|
|
727
|
+
# Display result
|
|
728
|
+
if headless or final_only:
|
|
729
|
+
# Headless/final-only: render markdown to stdout
|
|
730
|
+
from rich.markdown import Markdown
|
|
731
|
+
|
|
732
|
+
from tsugite.console import get_stdout_console
|
|
733
|
+
|
|
734
|
+
get_stdout_console(no_color=no_color, force_terminal=True).print(Markdown(result_str))
|
|
735
|
+
else:
|
|
736
|
+
# Show completion banner to stderr
|
|
737
|
+
stderr_console.print()
|
|
738
|
+
stderr_console.rule("[bold green]Agent Execution Complete[/bold green]")
|
|
739
|
+
# Output final result to stdout (for piping) with markdown rendering
|
|
740
|
+
from rich.markdown import Markdown
|
|
741
|
+
|
|
742
|
+
from tsugite.console import get_stdout_console
|
|
743
|
+
|
|
744
|
+
get_stdout_console(no_color=no_color, force_terminal=True).print(Markdown(result_str))
|
|
745
|
+
|
|
746
|
+
except ValueError as e:
|
|
747
|
+
get_error_console(headless, console).print(f"[red]Configuration error: {e}[/red]")
|
|
748
|
+
raise typer.Exit(1)
|
|
749
|
+
except RuntimeError as e:
|
|
750
|
+
get_error_console(headless, console).print(f"[red]Execution error: {e}[/red]")
|
|
751
|
+
raise typer.Exit(1)
|
|
752
|
+
except KeyboardInterrupt:
|
|
753
|
+
get_error_console(headless, console).print("\n[yellow]Agent execution interrupted by user[/yellow]")
|
|
754
|
+
raise typer.Exit(130)
|
|
755
|
+
except Exception as e:
|
|
756
|
+
err_console = get_error_console(headless, console)
|
|
757
|
+
err_console.print(f"[red]Unexpected error: {e}[/red]")
|
|
758
|
+
if not log_json:
|
|
759
|
+
err_console.print("\n[dim]Use --log-json for machine-readable output[/dim]")
|
|
760
|
+
raise typer.Exit(1)
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
@app.command()
|
|
764
|
+
def render(
|
|
765
|
+
agent_path: Optional[str] = typer.Argument(
|
|
766
|
+
None, help="Path to agent markdown file or builtin agent name (optional when using --continue)"
|
|
767
|
+
),
|
|
768
|
+
prompt: Optional[str] = typer.Argument(default="", help="Prompt/task for the agent (optional)"),
|
|
769
|
+
root: Optional[str] = typer.Option(None, "--root", help="Working directory"),
|
|
770
|
+
no_color: bool = typer.Option(False, "--no-color", help="Disable ANSI colors"),
|
|
771
|
+
verbose: bool = typer.Option(False, "--verbose", help="Show full attachment content (default: truncated)"),
|
|
772
|
+
raw: bool = typer.Option(False, "--raw", help="Show raw Jinja templates in instructions without rendering"),
|
|
773
|
+
attachment: Optional[List[str]] = typer.Option(
|
|
774
|
+
None, "-f", "--attachment", help="Attachment(s) to include (repeatable)"
|
|
775
|
+
),
|
|
776
|
+
refresh_cache: bool = typer.Option(False, "--refresh-cache", help="Force refresh cached attachment content"),
|
|
777
|
+
auto_context: Optional[bool] = typer.Option(
|
|
778
|
+
None,
|
|
779
|
+
"--auto-context/--no-auto-context",
|
|
780
|
+
help="Enable/disable auto-context attachments (overrides config/agent)",
|
|
781
|
+
),
|
|
782
|
+
continue_conversation: bool = typer.Option(
|
|
783
|
+
False, "--continue", "-c", help="Show prompt for continuing conversation"
|
|
784
|
+
),
|
|
785
|
+
conversation_id: Optional[str] = typer.Option(
|
|
786
|
+
None, "--conversation-id", help="Specific conversation ID (use with --continue)"
|
|
787
|
+
),
|
|
788
|
+
):
|
|
789
|
+
"""Render an agent template without executing it.
|
|
790
|
+
|
|
791
|
+
Examples:
|
|
792
|
+
tsu render agent.md "prompt"
|
|
793
|
+
tsu render +builtin-default "prompt"
|
|
794
|
+
tsu render builtin-default "prompt"
|
|
795
|
+
tsu render --continue "prompt" # Auto-detects agent
|
|
796
|
+
tsu render agent.md "prompt" --continue
|
|
797
|
+
tsu render --continue --conversation-id CONV_ID "prompt"
|
|
798
|
+
"""
|
|
799
|
+
# Lazy imports
|
|
800
|
+
from tsugite.agent_preparation import AgentPreparer
|
|
801
|
+
|
|
802
|
+
if no_color:
|
|
803
|
+
console.no_color = True
|
|
804
|
+
|
|
805
|
+
# Handle conversation continuation
|
|
806
|
+
continue_conversation_id = None
|
|
807
|
+
if continue_conversation:
|
|
808
|
+
from tsugite.ui.chat_history import get_latest_conversation
|
|
809
|
+
|
|
810
|
+
if conversation_id:
|
|
811
|
+
continue_conversation_id = conversation_id
|
|
812
|
+
console.print(f"[cyan]Rendering for conversation: {continue_conversation_id}[/cyan]")
|
|
813
|
+
else:
|
|
814
|
+
continue_conversation_id = get_latest_conversation()
|
|
815
|
+
if not continue_conversation_id:
|
|
816
|
+
console.print("[red]No conversations found[/red]")
|
|
817
|
+
raise typer.Exit(1)
|
|
818
|
+
console.print(f"[cyan]Rendering for latest conversation: {continue_conversation_id}[/cyan]")
|
|
819
|
+
|
|
820
|
+
# Auto-detect agent from conversation if not specified
|
|
821
|
+
if continue_conversation_id and not agent_path:
|
|
822
|
+
from tsugite.history import get_conversation_metadata
|
|
823
|
+
|
|
824
|
+
metadata = get_conversation_metadata(continue_conversation_id)
|
|
825
|
+
if not metadata:
|
|
826
|
+
console.print(f"[red]Could not load metadata for conversation: {continue_conversation_id}[/red]")
|
|
827
|
+
raise typer.Exit(1)
|
|
828
|
+
|
|
829
|
+
agent_path = f"+{metadata.agent}"
|
|
830
|
+
console.print(f"[cyan]Auto-detected agent from conversation: {metadata.agent}[/cyan]")
|
|
831
|
+
|
|
832
|
+
# Validate agent_path is provided
|
|
833
|
+
if not agent_path:
|
|
834
|
+
console.print("[red]Error: AGENT_PATH is required (or use --continue to auto-detect)[/red]")
|
|
835
|
+
raise typer.Exit(1)
|
|
836
|
+
|
|
837
|
+
with change_to_root_directory(root, console):
|
|
838
|
+
try:
|
|
839
|
+
# Load and validate agent (handles both builtin and file-based agents)
|
|
840
|
+
agent, agent_file_path, agent_display_name = load_and_validate_agent(agent_path, console)
|
|
841
|
+
|
|
842
|
+
base_dir = Path.cwd()
|
|
843
|
+
|
|
844
|
+
# Inject auto-context if enabled
|
|
845
|
+
agent_attachments = inject_auto_context_if_enabled(
|
|
846
|
+
agent.config.attachments,
|
|
847
|
+
agent.config.auto_context,
|
|
848
|
+
cli_override=auto_context,
|
|
849
|
+
)
|
|
850
|
+
|
|
851
|
+
prompt_updated, resolved_attachments = assemble_prompt_with_attachments(
|
|
852
|
+
prompt=prompt,
|
|
853
|
+
agent_attachments=agent_attachments,
|
|
854
|
+
cli_attachments=attachment,
|
|
855
|
+
base_dir=base_dir,
|
|
856
|
+
refresh_cache=refresh_cache,
|
|
857
|
+
console=console,
|
|
858
|
+
)
|
|
859
|
+
|
|
860
|
+
# Build context
|
|
861
|
+
# Note: Conversation history is no longer loaded here because chat_history
|
|
862
|
+
# template blocks have been removed from modern agents. History is now
|
|
863
|
+
# handled via previous_messages in agent execution, not template rendering.
|
|
864
|
+
context = {}
|
|
865
|
+
if continue_conversation_id:
|
|
866
|
+
# Enable text_mode when continuing (matches run behavior)
|
|
867
|
+
agent.config.text_mode = True
|
|
868
|
+
|
|
869
|
+
# Prepare agent (all rendering + tool building logic)
|
|
870
|
+
preparer = AgentPreparer()
|
|
871
|
+
prepared = preparer.prepare(
|
|
872
|
+
agent=agent,
|
|
873
|
+
prompt=prompt_updated,
|
|
874
|
+
skip_tool_directives=True, # Render doesn't execute tool directives
|
|
875
|
+
context=context,
|
|
876
|
+
attachments=resolved_attachments, # Pass attachments separately
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
# Display what will be sent to LLM
|
|
880
|
+
console.print(
|
|
881
|
+
Panel(
|
|
882
|
+
f"[cyan]Agent:[/cyan] {agent_display_name}\n"
|
|
883
|
+
f"[cyan]Prompt:[/cyan] {prompt}\n"
|
|
884
|
+
f"[cyan]Directory:[/cyan] {Path.cwd()}",
|
|
885
|
+
title="Tsugite Template Renderer",
|
|
886
|
+
border_style="green",
|
|
887
|
+
)
|
|
888
|
+
)
|
|
889
|
+
|
|
890
|
+
# Show message structure
|
|
891
|
+
console.print()
|
|
892
|
+
console.rule(
|
|
893
|
+
"[bold yellow]MESSAGE STRUCTURE[/bold yellow] [dim](sent to LLM as separate content blocks)[/dim]",
|
|
894
|
+
style="yellow",
|
|
895
|
+
)
|
|
896
|
+
|
|
897
|
+
# Message 1: System (role: system)
|
|
898
|
+
console.print()
|
|
899
|
+
console.rule("[bold cyan]Message 1: System Role[/bold cyan]", style="cyan", align="left")
|
|
900
|
+
|
|
901
|
+
# Content Block 1: System Instructions
|
|
902
|
+
console.print()
|
|
903
|
+
console.rule("[dim]Content Block 1: System Instructions[/dim]", style="dim", align="left")
|
|
904
|
+
console.print(prepared.system_message)
|
|
905
|
+
|
|
906
|
+
# Display attachments if they exist (as additional content blocks in system message)
|
|
907
|
+
if prepared.attachments:
|
|
908
|
+
for idx, (name, content) in enumerate(prepared.attachments, start=2):
|
|
909
|
+
console.print()
|
|
910
|
+
console.rule(
|
|
911
|
+
f"[dim]Content Block {idx}: Attachment - {name}[/dim]",
|
|
912
|
+
style="dim",
|
|
913
|
+
align="left",
|
|
914
|
+
)
|
|
915
|
+
console.print(f"[yellow]<Attachment: {name}>[/yellow]")
|
|
916
|
+
|
|
917
|
+
# Truncate unless verbose
|
|
918
|
+
if verbose:
|
|
919
|
+
console.print(content)
|
|
920
|
+
else:
|
|
921
|
+
# Show first 10 lines and last 5 lines
|
|
922
|
+
lines = content.split("\n")
|
|
923
|
+
if len(lines) > 20:
|
|
924
|
+
preview = "\n".join(lines[:10])
|
|
925
|
+
preview += (
|
|
926
|
+
f"\n[dim]... ({len(lines) - 15} lines truncated, use --verbose to see all) ...[/dim]\n"
|
|
927
|
+
)
|
|
928
|
+
preview += "\n".join(lines[-5:])
|
|
929
|
+
console.print(preview)
|
|
930
|
+
else:
|
|
931
|
+
console.print(content)
|
|
932
|
+
|
|
933
|
+
console.print(f"[yellow]</Attachment: {name}>[/yellow]")
|
|
934
|
+
|
|
935
|
+
# Message 2: User (role: user)
|
|
936
|
+
console.print()
|
|
937
|
+
console.rule("[bold cyan]Message 2: User Role[/bold cyan]", style="cyan", align="left")
|
|
938
|
+
console.print()
|
|
939
|
+
console.rule("[dim]Content: User Task/Prompt[/dim]", style="dim", align="left")
|
|
940
|
+
console.print(prepared.user_message)
|
|
941
|
+
console.print()
|
|
942
|
+
console.rule(style="dim")
|
|
943
|
+
|
|
944
|
+
except Exception as e:
|
|
945
|
+
console.print(f"[red]Render error: {e}[/red]")
|
|
946
|
+
raise typer.Exit(1)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
@app.command()
|
|
950
|
+
def version():
|
|
951
|
+
"""Show version information."""
|
|
952
|
+
from tsugite import __version__
|
|
953
|
+
|
|
954
|
+
console.print(f"Tsugite version {__version__}")
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
@app.command()
|
|
958
|
+
def chat(
|
|
959
|
+
agent: Optional[str] = typer.Argument(None, help="Agent name or path (optional, uses default if not provided)"),
|
|
960
|
+
model: Optional[str] = typer.Option(None, "--model", help="Override agent model"),
|
|
961
|
+
max_history: int = typer.Option(DEFAULT_MAX_CHAT_HISTORY, "--max-history", help="Maximum turns to keep in context"),
|
|
962
|
+
stream: bool = typer.Option(False, "--stream", help="Stream LLM responses in real-time"),
|
|
963
|
+
no_history: bool = typer.Option(False, "--no-history", help="Disable conversation history persistence"),
|
|
964
|
+
continue_: Optional[str] = typer.Option(
|
|
965
|
+
None, "--continue", "-c", help="Resume conversation by ID, or latest if no ID given"
|
|
966
|
+
),
|
|
967
|
+
root: Optional[str] = typer.Option(None, "--root", help="Working directory"),
|
|
968
|
+
):
|
|
969
|
+
"""Start an interactive chat session with an agent."""
|
|
970
|
+
from tsugite.agent_composition import parse_agent_references
|
|
971
|
+
from tsugite.ui.textual_chat import run_textual_chat
|
|
972
|
+
|
|
973
|
+
with change_to_root_directory(root, console):
|
|
974
|
+
# Handle conversation resume
|
|
975
|
+
resume_conversation_id = None
|
|
976
|
+
resume_turns = None
|
|
977
|
+
|
|
978
|
+
if continue_ is not None:
|
|
979
|
+
from tsugite.ui.chat_history import get_latest_conversation, load_conversation_history
|
|
980
|
+
|
|
981
|
+
# Determine conversation ID to resume
|
|
982
|
+
if continue_ == "" or continue_.lower() == "latest":
|
|
983
|
+
resume_conversation_id = get_latest_conversation()
|
|
984
|
+
if not resume_conversation_id:
|
|
985
|
+
console.print("[red]No conversations found to resume[/red]")
|
|
986
|
+
raise typer.Exit(1)
|
|
987
|
+
console.print(f"[cyan]Resuming latest conversation: {resume_conversation_id}[/cyan]")
|
|
988
|
+
else:
|
|
989
|
+
resume_conversation_id = continue_
|
|
990
|
+
console.print(f"[cyan]Resuming conversation: {resume_conversation_id}[/cyan]")
|
|
991
|
+
|
|
992
|
+
# Load conversation history
|
|
993
|
+
try:
|
|
994
|
+
resume_turns = load_conversation_history(resume_conversation_id)
|
|
995
|
+
console.print(f"[cyan]Loaded {len(resume_turns)} previous turns[/cyan]")
|
|
996
|
+
except FileNotFoundError:
|
|
997
|
+
console.print(f"[red]Conversation not found: {resume_conversation_id}[/red]")
|
|
998
|
+
raise typer.Exit(1)
|
|
999
|
+
except Exception as e:
|
|
1000
|
+
console.print(f"[red]Failed to load conversation: {e}[/red]")
|
|
1001
|
+
raise typer.Exit(1)
|
|
1002
|
+
|
|
1003
|
+
# Resolve agent path
|
|
1004
|
+
if agent:
|
|
1005
|
+
# Parse agent reference
|
|
1006
|
+
base_dir = Path.cwd()
|
|
1007
|
+
agent_refs = [agent]
|
|
1008
|
+
primary_agent_path, _ = parse_agent_references(agent_refs, None, base_dir)
|
|
1009
|
+
else:
|
|
1010
|
+
# Use package-provided chat assistant by default
|
|
1011
|
+
# Users can override by creating .tsugite/chat_assistant.md or agents/chat_assistant.md
|
|
1012
|
+
base_dir = Path.cwd()
|
|
1013
|
+
agent_refs = ["chat-assistant"]
|
|
1014
|
+
primary_agent_path, _ = parse_agent_references(agent_refs, None, base_dir)
|
|
1015
|
+
|
|
1016
|
+
# Validate agent file exists
|
|
1017
|
+
if not primary_agent_path.exists():
|
|
1018
|
+
console.print(f"[red]Agent file not found: {primary_agent_path}[/red]")
|
|
1019
|
+
raise typer.Exit(1)
|
|
1020
|
+
|
|
1021
|
+
# Run chat with Textual UI
|
|
1022
|
+
run_textual_chat(
|
|
1023
|
+
agent_path=primary_agent_path,
|
|
1024
|
+
model_override=model,
|
|
1025
|
+
max_history=max_history,
|
|
1026
|
+
stream=stream,
|
|
1027
|
+
disable_history=no_history,
|
|
1028
|
+
resume_conversation_id=resume_conversation_id,
|
|
1029
|
+
resume_turns=resume_turns,
|
|
1030
|
+
)
|
|
1031
|
+
|
|
1032
|
+
|
|
1033
|
+
# Register subcommands from separate modules
|
|
1034
|
+
app.add_typer(mcp_app, name="mcp")
|
|
1035
|
+
app.add_typer(agents_app, name="agents")
|
|
1036
|
+
app.add_typer(config_app, name="config")
|
|
1037
|
+
app.add_typer(attachments_app, name="attachments")
|
|
1038
|
+
app.add_typer(cache_app, name="cache")
|
|
1039
|
+
app.add_typer(tools_app, name="tools")
|
|
1040
|
+
app.add_typer(history_app, name="history")
|
|
1041
|
+
app.command("benchmark")(benchmark_command)
|
|
1042
|
+
app.command("init")(init)
|