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,529 @@
1
+ """Live UI handler template using Rich Live Display, Tree, and Prompt.
2
+
3
+ This is a TEMPLATE showing how to create a custom UI mode with:
4
+ - Live Display for dynamic updates
5
+ - Tree for showing execution hierarchy
6
+ - Prompt for interactive elements
7
+
8
+ To use this template:
9
+ 1. Rename this file (e.g., live_interactive.py)
10
+ 2. Rename the class (e.g., LiveInteractiveHandler)
11
+ 3. Customize the rendering methods for your needs
12
+ 4. Add helper function in ui/helpers.py
13
+ 5. Export in ui/__init__.py
14
+ 6. Integrate in cli/__init__.py
15
+
16
+ See CLAUDE.md for full UI system documentation.
17
+ """
18
+
19
+ from contextlib import contextmanager
20
+ from typing import Generator, Optional
21
+
22
+ from rich.console import Console
23
+ from rich.layout import Layout
24
+ from rich.live import Live
25
+ from rich.panel import Panel
26
+ from rich.prompt import Confirm, Prompt
27
+ from rich.table import Table
28
+ from rich.tree import Tree
29
+
30
+ from tsugite.events import (
31
+ CodeExecutionEvent,
32
+ CostSummaryEvent,
33
+ ErrorEvent,
34
+ FinalAnswerEvent,
35
+ ObservationEvent,
36
+ ReasoningContentEvent,
37
+ StepStartEvent,
38
+ TaskStartEvent,
39
+ ToolCallEvent,
40
+ )
41
+ from tsugite.ui.base import CustomUIHandler
42
+
43
+
44
+ class LiveTemplateHandler(CustomUIHandler):
45
+ """Template UI handler using Rich Live Display with Tree and interactive prompts.
46
+
47
+ Features demonstrated:
48
+ - Live Display: Real-time updates without scrolling
49
+ - Tree: Hierarchical view of execution steps
50
+ - Prompt: Interactive user input during execution
51
+ - Layout: Multi-panel interface
52
+
53
+ Customize this to fit your needs!
54
+ """
55
+
56
+ def __init__(self, console: Console = None, interactive: bool = True):
57
+ """Initialize the live template handler.
58
+
59
+ Args:
60
+ console: Rich console instance (creates stderr console if None)
61
+ interactive: Whether to enable interactive prompts
62
+ """
63
+ if console is None:
64
+ from tsugite.console import get_stderr_console
65
+
66
+ console = get_stderr_console()
67
+
68
+ # Initialize parent with your preferred flags
69
+ super().__init__(
70
+ console=console,
71
+ show_code=True, # Customize these
72
+ show_observations=True,
73
+ show_llm_messages=False,
74
+ show_execution_results=True,
75
+ show_execution_logs=True,
76
+ show_panels=False, # We'll use Live Display instead
77
+ )
78
+
79
+ # Live display components
80
+ self.live_display: Optional[Live] = None
81
+ self.execution_tree: Optional[Tree] = None
82
+ self.interactive = interactive
83
+
84
+ # Track current state for live updates
85
+ self.current_status = "Initializing..."
86
+ self.step_count = 0
87
+ self.tool_calls = []
88
+ self.errors = []
89
+
90
+ @contextmanager
91
+ def progress_context(self) -> Generator[None, None, None]:
92
+ """Context manager for Live Display during execution.
93
+
94
+ This replaces the default progress spinner with a full Live Display.
95
+ """
96
+ from tsugite.ui_context import clear_ui_context, set_ui_context
97
+
98
+ # Create the layout
99
+ layout = self._create_layout()
100
+
101
+ # Create Live Display
102
+ self.live_display = Live(
103
+ layout,
104
+ console=self.console,
105
+ refresh_per_second=4, # Adjust refresh rate
106
+ screen=False, # Set to True for full-screen mode
107
+ )
108
+
109
+ # Store console and ui_handler in thread-local for tool access
110
+ set_ui_context(console=self.console, progress=None, ui_handler=self)
111
+
112
+ with self.live_display:
113
+ try:
114
+ yield
115
+ finally:
116
+ self.live_display = None
117
+ clear_ui_context()
118
+
119
+ @contextmanager
120
+ def pause_for_input(self) -> Generator[None, None, None]:
121
+ """Pause the Live Display for user input.
122
+
123
+ Stops the live display, shows the prompt, then restarts it.
124
+ """
125
+ if self.live_display is not None:
126
+ self.live_display.stop()
127
+
128
+ try:
129
+ yield
130
+ finally:
131
+ if self.live_display is not None:
132
+ self.live_display.start()
133
+
134
+ def _create_layout(self) -> Layout:
135
+ """Create the layout structure for Live Display.
136
+
137
+ Customize this to arrange your panels however you want.
138
+
139
+ Returns:
140
+ Layout with configured panels
141
+ """
142
+ # Create main layout with multiple sections
143
+ layout = Layout()
144
+
145
+ # Split into header, body, footer
146
+ layout.split(
147
+ Layout(name="header", size=3),
148
+ Layout(name="body"),
149
+ Layout(name="footer", size=3),
150
+ )
151
+
152
+ # Split body into left (tree) and right (details)
153
+ layout["body"].split_row(
154
+ Layout(name="tree", ratio=1),
155
+ Layout(name="details", ratio=2),
156
+ )
157
+
158
+ # Initial content
159
+ layout["header"].update(self._render_header())
160
+ layout["tree"].update(self._render_tree())
161
+ layout["details"].update(self._render_details())
162
+ layout["footer"].update(self._render_footer())
163
+
164
+ return layout
165
+
166
+ def _render_header(self) -> Panel:
167
+ """Render the header panel.
168
+
169
+ Returns:
170
+ Panel with header content
171
+ """
172
+ return Panel(
173
+ f"[bold cyan]Tsugite Agent Execution[/bold cyan] | Status: {self.current_status}",
174
+ style="cyan",
175
+ )
176
+
177
+ def _render_tree(self) -> Panel:
178
+ """Render the execution tree.
179
+
180
+ This shows the hierarchical structure of execution steps.
181
+
182
+ Returns:
183
+ Panel with tree content
184
+ """
185
+ if self.execution_tree is None:
186
+ # Create initial tree
187
+ self.execution_tree = Tree("🚀 [bold]Execution", guide_style="dim")
188
+
189
+ return Panel(self.execution_tree, title="[bold]Execution Tree[/bold]", border_style="green")
190
+
191
+ def _render_details(self) -> Panel:
192
+ """Render the details panel.
193
+
194
+ Shows current step information, code, observations, etc.
195
+
196
+ Returns:
197
+ Panel with detail content
198
+ """
199
+ # Build details from current state
200
+ details = []
201
+
202
+ if self.state.task:
203
+ details.append(f"[bold]Task:[/bold] {self.state.task}")
204
+ details.append("")
205
+
206
+ if self.state.current_step:
207
+ details.append(f"[bold]Step:[/bold] {self.state.current_step}")
208
+
209
+ if self.state.code_being_executed:
210
+ details.append("\n[bold yellow]Executing Code:[/bold yellow]")
211
+ # Truncate long code
212
+ code_preview = self.state.code_being_executed[:200]
213
+ if len(self.state.code_being_executed) > 200:
214
+ code_preview += "..."
215
+ details.append(f"[dim]{code_preview}[/dim]")
216
+
217
+ # Show recent tool calls
218
+ if self.tool_calls:
219
+ details.append("\n[bold]Recent Tools:[/bold]")
220
+ for tool in self.tool_calls[-3:]: # Last 3 tools
221
+ details.append(f" • {tool}")
222
+
223
+ # Show errors if any
224
+ if self.errors:
225
+ details.append("\n[bold red]Errors:[/bold red]")
226
+ for error in self.errors[-2:]: # Last 2 errors
227
+ details.append(f" ⚠️ {error}")
228
+
229
+ content = "\n".join(details) if details else "[dim]No details yet...[/dim]"
230
+ return Panel(content, title="[bold]Details[/bold]", border_style="blue")
231
+
232
+ def _render_footer(self) -> Panel:
233
+ """Render the footer panel.
234
+
235
+ Shows summary statistics, costs, etc.
236
+
237
+ Returns:
238
+ Panel with footer content
239
+ """
240
+ stats = f"Steps: {self.step_count} | Tools: {len(self.tool_calls)} | Errors: {len(self.errors)}"
241
+ return Panel(stats, style="dim")
242
+
243
+ def _update_live_display(self):
244
+ """Update the live display with current state.
245
+
246
+ Call this whenever you want to refresh the display.
247
+ """
248
+ if self.live_display and self.live_display.is_started:
249
+ layout = self._create_layout()
250
+ self.live_display.update(layout)
251
+
252
+ # ============================================================================
253
+ # Event Handlers - Override these to customize behavior
254
+ # ============================================================================
255
+
256
+ def _handle_task_start(self, event: TaskStartEvent) -> None:
257
+ """Handle task start event."""
258
+ self.state.task = event.task
259
+ self.current_status = "Starting..."
260
+
261
+ # Create root of execution tree
262
+ self.execution_tree = Tree(
263
+ (
264
+ f"🚀 [bold]{self.state.task[:50]}...[/bold]"
265
+ if len(self.state.task or "") > 50
266
+ else f"🚀 [bold]{self.state.task}[/bold]"
267
+ ),
268
+ guide_style="dim",
269
+ )
270
+
271
+ self._update_live_display()
272
+
273
+ def _handle_step_start(self, event: StepStartEvent) -> None:
274
+ """Handle step start event."""
275
+ self.state.current_step = event.step
276
+ self.step_count = self.state.current_step
277
+ self.current_status = f"Step {self.state.current_step}: Thinking..."
278
+
279
+ # Add step to tree
280
+ if self.execution_tree:
281
+ step_label = (
282
+ f"Round {self.state.current_step}"
283
+ if self.state.multistep_context
284
+ else f"Step {self.state.current_step}"
285
+ )
286
+ self.execution_tree.add(f"🤔 {step_label}", style="cyan")
287
+
288
+ self._update_live_display()
289
+
290
+ def _handle_code_execution(self, event: CodeExecutionEvent) -> None:
291
+ """Handle code execution event."""
292
+ self.state.code_being_executed = event.code
293
+ self.current_status = f"Step {self.state.current_step}: Executing code..."
294
+
295
+ # Add code execution to tree
296
+ if self.execution_tree and self.execution_tree.children:
297
+ last_step = self.execution_tree.children[-1]
298
+ code_preview = (
299
+ self.state.code_being_executed[:40] + "..."
300
+ if len(self.state.code_being_executed) > 40
301
+ else self.state.code_being_executed
302
+ )
303
+ last_step.add(f"⚡ Code: {code_preview}", style="yellow")
304
+
305
+ self._update_live_display()
306
+
307
+ def _handle_tool_call(self, event: ToolCallEvent) -> None:
308
+ """Handle tool call event."""
309
+ content = event.tool
310
+
311
+ # Extract tool name from content
312
+ if "Calling tool:" in content:
313
+ tool_name = content.split("Calling tool:")[1].split("with")[0].strip()
314
+ self.tool_calls.append(tool_name)
315
+ self.current_status = f"Step {self.state.current_step}: Using {tool_name}..."
316
+
317
+ # Add to tree
318
+ if self.execution_tree and self.execution_tree.children:
319
+ last_step = self.execution_tree.children[-1]
320
+ last_step.add(f"🔧 {tool_name}", style="magenta")
321
+
322
+ self._update_live_display()
323
+
324
+ def _handle_observation(self, event: ObservationEvent) -> None:
325
+ """Handle observation event."""
326
+ observation = event.observation
327
+ self.current_status = f"Step {self.state.current_step}: Processing results..."
328
+
329
+ # Check for errors in observation
330
+ is_error = any(keyword in observation.lower() for keyword in ["error", "failed", "exception", "traceback"])
331
+
332
+ if is_error:
333
+ self.errors.append(observation[:100])
334
+ # Add error to tree
335
+ if self.execution_tree and self.execution_tree.children:
336
+ last_step = self.execution_tree.children[-1]
337
+ last_step.add("❌ Error", style="red")
338
+ else:
339
+ # Add success to tree
340
+ if self.execution_tree and self.execution_tree.children:
341
+ last_step = self.execution_tree.children[-1]
342
+ obs_preview = observation[:40] + "..." if len(observation) > 40 else observation
343
+ last_step.add(f"✓ {obs_preview}", style="green")
344
+
345
+ self._update_live_display()
346
+
347
+ def _handle_final_answer(self, event: FinalAnswerEvent) -> None:
348
+ """Handle final answer event."""
349
+ answer = str(event.answer)
350
+ self.current_status = "✅ Complete"
351
+
352
+ # Add final answer to tree
353
+ if self.execution_tree:
354
+ self.execution_tree.add(f"✅ [bold green]Final Answer: {answer[:50]}...[/bold green]")
355
+
356
+ self._update_live_display()
357
+
358
+ # Optional: Show interactive confirmation
359
+ if self.interactive:
360
+ self.live_display.stop()
361
+ self.console.print("\n" + "=" * 60)
362
+ self.console.print(f"[bold green]Final Answer:[/bold green] {answer}")
363
+ self.console.print("=" * 60)
364
+
365
+ # Example of using Prompt
366
+ # satisfied = Confirm.ask("Are you satisfied with this answer?")
367
+ # if not satisfied:
368
+ # # Could trigger retry or refinement
369
+ # pass
370
+
371
+ def _handle_error(self, event: ErrorEvent) -> None:
372
+ """Handle error event."""
373
+ error = event.error
374
+ error_type = event.error_type or "Error"
375
+
376
+ self.errors.append(f"{error_type}: {error[:100]}")
377
+ self.current_status = f"❌ {error_type}"
378
+
379
+ # Add error to tree
380
+ if self.execution_tree:
381
+ self.execution_tree.add(f"❌ [bold red]{error_type}[/bold red]", style="red")
382
+
383
+ self._update_live_display()
384
+
385
+ def _handle_reasoning_content(self, event: ReasoningContentEvent) -> None:
386
+ """Handle reasoning content from models like Claude."""
387
+ content = event.content
388
+
389
+ # Add reasoning to tree
390
+ if self.execution_tree and content:
391
+ reasoning_preview = content[:50] + "..." if len(content) > 50 else content
392
+ self.execution_tree.add(f"🧠 Reasoning: {reasoning_preview}", style="magenta")
393
+
394
+ self._update_live_display()
395
+
396
+ def _handle_cost_summary(self, event: CostSummaryEvent) -> None:
397
+ """Handle cost summary display."""
398
+ cost = event.cost
399
+ total_tokens = event.tokens
400
+ cached_tokens = event.cached_tokens
401
+ # Cache fields available: event.cache_creation_input_tokens, event.cache_read_input_tokens
402
+
403
+ # Update footer with cost info
404
+ if cost or total_tokens or cached_tokens:
405
+ # Could update footer or show in details panel
406
+ # Example: Add cost/token/cache info to self.current_status or track in instance variable
407
+ pass
408
+
409
+ self._update_live_display()
410
+
411
+ # ============================================================================
412
+ # Interactive Prompt Examples
413
+ # ============================================================================
414
+
415
+ def prompt_for_confirmation(self, message: str) -> bool:
416
+ """Example: Prompt user for confirmation.
417
+
418
+ Args:
419
+ message: Confirmation message
420
+
421
+ Returns:
422
+ True if confirmed, False otherwise
423
+ """
424
+ if not self.interactive:
425
+ return True
426
+
427
+ # Pause live display for interaction
428
+ if self.live_display:
429
+ self.live_display.stop()
430
+
431
+ result = Confirm.ask(message)
432
+
433
+ # Resume live display
434
+ if self.live_display:
435
+ self.live_display.start()
436
+
437
+ return result
438
+
439
+ def prompt_for_input(self, message: str, default: str = "") -> str:
440
+ """Example: Prompt user for text input.
441
+
442
+ Args:
443
+ message: Prompt message
444
+ default: Default value
445
+
446
+ Returns:
447
+ User input
448
+ """
449
+ if not self.interactive:
450
+ return default
451
+
452
+ # Pause live display for interaction
453
+ if self.live_display:
454
+ self.live_display.stop()
455
+
456
+ result = Prompt.ask(message, default=default)
457
+
458
+ # Resume live display
459
+ if self.live_display:
460
+ self.live_display.start()
461
+
462
+ return result
463
+
464
+
465
+ # ============================================================================
466
+ # Example: Alternative implementation with Table instead of Tree
467
+ # ============================================================================
468
+
469
+
470
+ class LiveTableHandler(LiveTemplateHandler):
471
+ """Alternative template using Table for step tracking.
472
+
473
+ Shows how to create variants of the same UI pattern.
474
+ """
475
+
476
+ def __init__(self, console: Console = None, interactive: bool = True):
477
+ super().__init__(console, interactive)
478
+ self.steps_table = Table(show_header=True, header_style="bold cyan")
479
+ self.steps_table.add_column("Step", style="cyan", width=8)
480
+ self.steps_table.add_column("Status", width=12)
481
+ self.steps_table.add_column("Action", overflow="fold")
482
+
483
+ def _render_tree(self) -> Panel:
484
+ """Override to use Table instead of Tree."""
485
+ return Panel(
486
+ self.steps_table,
487
+ title="[bold]Execution Steps[/bold]",
488
+ border_style="green",
489
+ )
490
+
491
+ def _handle_step_start(self, event: StepStartEvent) -> None:
492
+ """Add step as table row."""
493
+ step = event.step
494
+ self.steps_table.add_row(
495
+ f"Step {step}",
496
+ "[yellow]Running[/yellow]",
497
+ "Waiting for LLM...",
498
+ )
499
+ self._update_live_display()
500
+
501
+ # Override other handlers as needed to update table instead of tree...
502
+
503
+
504
+ # ============================================================================
505
+ # Helper function (add this to ui/helpers.py)
506
+ # ============================================================================
507
+
508
+
509
+ def create_live_template_logger(interactive: bool = True):
510
+ """Create logger using Live Template handler.
511
+
512
+ Args:
513
+ interactive: Enable interactive prompts
514
+
515
+ Returns:
516
+ CustomUILogger with LiveTemplateHandler
517
+
518
+ Example:
519
+ from tsugite.ui.helpers import create_live_template_logger
520
+
521
+ logger = create_live_template_logger(interactive=True)
522
+ # Use with agent runner
523
+ """
524
+ from tsugite.ui.base import CustomUILogger
525
+
526
+ # from tsugite.ui.live_template import LiveTemplateHandler # After renaming
527
+
528
+ handler = LiveTemplateHandler(interactive=interactive)
529
+ return CustomUILogger(handler, handler.console)