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
tsugite/ui/base.py ADDED
@@ -0,0 +1,638 @@
1
+ """Base UI handler and core UI system components."""
2
+
3
+ import threading
4
+ from contextlib import contextmanager
5
+ from dataclasses import dataclass
6
+ from typing import Any, Dict, Generator, List, Optional
7
+
8
+ from rich.console import Console
9
+ from rich.markdown import Markdown
10
+ from rich.progress import Progress, SpinnerColumn, TextColumn
11
+ from rich.syntax import Syntax
12
+ from rich.text import Text
13
+
14
+ from tsugite.events import (
15
+ BaseEvent,
16
+ CodeExecutionEvent,
17
+ CostSummaryEvent,
18
+ DebugMessageEvent,
19
+ ErrorEvent,
20
+ ExecutionLogsEvent,
21
+ ExecutionResultEvent,
22
+ FinalAnswerEvent,
23
+ InfoEvent,
24
+ LLMMessageEvent,
25
+ ObservationEvent,
26
+ ReasoningContentEvent,
27
+ ReasoningTokensEvent,
28
+ StepProgressEvent,
29
+ StepStartEvent,
30
+ StreamChunkEvent,
31
+ StreamCompleteEvent,
32
+ TaskStartEvent,
33
+ ToolCallEvent,
34
+ WarningEvent,
35
+ )
36
+ from tsugite.ui_context import clear_ui_context, set_ui_context
37
+
38
+
39
+ @dataclass
40
+ class UIState:
41
+ """Tracks the current state of the agent execution."""
42
+
43
+ task: Optional[str] = None
44
+ current_step: int = 0
45
+ total_steps: Optional[int] = None
46
+ code_being_executed: Optional[str] = None
47
+ steps_history: List[Dict[str, Any]] = None
48
+ multistep_context: Optional[Dict[str, Any]] = None
49
+
50
+ def __post_init__(self):
51
+ if self.steps_history is None:
52
+ self.steps_history = []
53
+
54
+
55
+ class CustomUILogger:
56
+ """Simple logger wrapper for TsugiteAgent.
57
+
58
+ Provides console and ui_handler access for displaying reasoning content
59
+ and multi-step progress. This is a minimal wrapper providing access to
60
+ the UI handler and console for rendering.
61
+ """
62
+
63
+ def __init__(self, ui_handler: "CustomUIHandler", console: Console):
64
+ """Initialize logger.
65
+
66
+ Args:
67
+ ui_handler: Handler for UI events
68
+ console: Rich console for output
69
+ """
70
+ self.ui_handler = ui_handler
71
+ self.console = console
72
+
73
+
74
+ class CustomUIHandler:
75
+ """Handles UI events and displays custom progress interface."""
76
+
77
+ def __init__(
78
+ self,
79
+ console: Console,
80
+ show_code: bool = True,
81
+ show_observations: bool = True,
82
+ show_llm_messages: bool = False,
83
+ show_execution_results: bool = True,
84
+ show_execution_logs: bool = True,
85
+ show_panels: bool = True,
86
+ show_debug_messages: bool = False,
87
+ ):
88
+ self.console = console
89
+ self.state = UIState()
90
+ self.show_code = show_code
91
+ self.show_observations = show_observations
92
+ self.show_llm_messages = show_llm_messages
93
+ self.show_execution_results = show_execution_results
94
+ self.show_execution_logs = show_execution_logs
95
+ self.show_panels = show_panels
96
+ self.show_debug_messages = show_debug_messages
97
+ self.progress = None
98
+ self.task_id = None
99
+ self._lock = threading.Lock()
100
+
101
+ # Streaming state
102
+ self.streaming_content = ""
103
+ self.is_streaming = False
104
+
105
+ def _print(self, *args, **kwargs) -> None:
106
+ """Print helper that uses progress.console when progress is active.
107
+
108
+ This ensures output doesn't interfere with Rich's Progress Live display.
109
+ When Progress Live is active, direct console.print() calls break rendering.
110
+ """
111
+ if self.progress:
112
+ self.progress.console.print(*args, **kwargs)
113
+ else:
114
+ self.console.print(*args, **kwargs)
115
+
116
+ def handle_event(self, event: BaseEvent) -> None:
117
+ """Handle a UI event and update the display."""
118
+ with self._lock:
119
+ if isinstance(event, TaskStartEvent):
120
+ self._handle_task_start(event)
121
+ elif isinstance(event, StepStartEvent):
122
+ self._handle_step_start(event)
123
+ elif isinstance(event, CodeExecutionEvent):
124
+ self._handle_code_execution(event)
125
+ elif isinstance(event, ToolCallEvent):
126
+ self._handle_tool_call(event)
127
+ elif isinstance(event, ObservationEvent):
128
+ self._handle_observation(event)
129
+ elif isinstance(event, FinalAnswerEvent):
130
+ self._handle_final_answer(event)
131
+ elif isinstance(event, ErrorEvent):
132
+ self._handle_error(event)
133
+ elif isinstance(event, LLMMessageEvent):
134
+ self._handle_llm_message(event)
135
+ elif isinstance(event, ExecutionResultEvent):
136
+ self._handle_execution_result(event)
137
+ elif isinstance(event, ExecutionLogsEvent):
138
+ self._handle_execution_logs(event)
139
+ elif isinstance(event, ReasoningContentEvent):
140
+ self._handle_reasoning_content(event)
141
+ elif isinstance(event, ReasoningTokensEvent):
142
+ self._handle_reasoning_tokens(event)
143
+ elif isinstance(event, CostSummaryEvent):
144
+ self._handle_cost_summary(event)
145
+ elif isinstance(event, StreamChunkEvent):
146
+ self._handle_stream_chunk(event)
147
+ elif isinstance(event, StreamCompleteEvent):
148
+ self._handle_stream_complete(event)
149
+ elif isinstance(event, InfoEvent):
150
+ self._handle_info(event)
151
+ elif isinstance(event, DebugMessageEvent):
152
+ self._handle_debug_message(event)
153
+ elif isinstance(event, WarningEvent):
154
+ self._handle_warning(event)
155
+ elif isinstance(event, StepProgressEvent):
156
+ self._handle_step_progress(event)
157
+
158
+ self._update_display()
159
+
160
+ def _get_display_prefix(self) -> str:
161
+ """Get display prefix for nested multi-step context."""
162
+ if self.state.multistep_context:
163
+ return " └─ "
164
+ return ""
165
+
166
+ @staticmethod
167
+ def _contains_error(text: str) -> bool:
168
+ """Check if text contains error keywords.
169
+
170
+ Args:
171
+ text: Text to check for error keywords
172
+
173
+ Returns:
174
+ True if text contains error indicators
175
+ """
176
+ error_keywords = ["error", "failed", "exception", "not found", "invalid", "traceback"]
177
+ return any(keyword in text.lower() for keyword in error_keywords)
178
+
179
+ def _handle_task_start(self, event: TaskStartEvent) -> None:
180
+ """Handle task start event."""
181
+ self.state.task = event.task
182
+ self.state.current_step = 0
183
+ self.state.steps_history = []
184
+
185
+ # Show task start in minimal mode (panels removed)
186
+ # Only show full prompt with --verbose, otherwise just show model
187
+ if self.show_debug_messages:
188
+ self._print(f"[bold]Task:[/bold] {self.state.task}")
189
+ self._print(f"[dim]Model: {event.model}[/dim]")
190
+ self._print("")
191
+
192
+ def _handle_step_start(self, event: StepStartEvent) -> None:
193
+ """Handle step start event."""
194
+ self.state.current_step = event.step
195
+
196
+ prefix = self._get_display_prefix()
197
+
198
+ # Show "Turn" for reasoning iterations
199
+ # (workflow steps are shown separately in multistep_context)
200
+ label = f"Turn {self.state.current_step}"
201
+
202
+ # Update progress
203
+ self.update_progress(f"{prefix}🤔 {label}: Waiting for LLM response...")
204
+
205
+ # Add step to history
206
+ self.state.steps_history.append({"step": self.state.current_step, "status": "in_progress", "actions": []})
207
+
208
+ def _handle_code_execution(self, event: CodeExecutionEvent) -> None:
209
+ """Handle code execution event."""
210
+ self.state.code_being_executed = event.code
211
+
212
+ prefix = self._get_display_prefix()
213
+ # Update progress
214
+ self.update_progress(f"{prefix}⚡ Executing code...")
215
+
216
+ if self.show_code and self.state.code_being_executed:
217
+ # Show code without panel (progress indicator already shown above)
218
+ self._print("")
219
+ self._print(Syntax(self.state.code_being_executed, "python", theme="monokai", background_color="default"))
220
+ self._print("")
221
+ elif not self.show_code:
222
+ # Code display disabled - just show indicator
223
+ self._print("[dim yellow]⚡ Executing code...[/dim yellow]")
224
+
225
+ def _handle_tool_call(self, event: ToolCallEvent) -> None:
226
+ """Handle tool call event."""
227
+ content = event.tool
228
+
229
+ prefix = self._get_display_prefix()
230
+ # Update progress
231
+ self.update_progress(f"{prefix}🔧 Calling tool...")
232
+
233
+ # In minimal mode, show lightweight indicator with tool name
234
+ if not self.show_panels and content:
235
+ # Parse tool name from content (format: "Tool: tool_name")
236
+ tool_name = content.replace("Tool: ", "").strip() if "Tool: " in content else content
237
+ self._print(f"[dim cyan]🔧 Called: {tool_name}[/dim cyan]")
238
+
239
+ # Add to current step history
240
+ if self.state.steps_history:
241
+ self.state.steps_history[-1]["actions"].append({"type": "tool_call", "content": content})
242
+
243
+ def _handle_observation(self, event: ObservationEvent) -> None:
244
+ """Handle observation event."""
245
+ observation = event.observation
246
+
247
+ prefix = self._get_display_prefix()
248
+ # Update progress
249
+ self.update_progress(f"{prefix}💡 Processing results...")
250
+
251
+ if observation:
252
+ # Clean up observation for display
253
+ clean_obs = observation.replace("|", "[").strip()
254
+
255
+ # Check if this is a final answer
256
+ is_final_answer = "__FINAL_ANSWER__:" in clean_obs
257
+
258
+ # Check if this looks like an error using shared helper
259
+ is_error = self._contains_error(clean_obs)
260
+
261
+ # Always show observations in minimal mode (panels removed, no filtering)
262
+ if is_error:
263
+ # Display errors prominently in red without truncation
264
+ self._print(f"[red]⚠️ {clean_obs}[/red]")
265
+ elif is_final_answer:
266
+ # Skip displaying final answer here - it will be displayed by _handle_final_answer event
267
+ pass
268
+ elif self.show_observations:
269
+ # Normal observation - show with truncation if needed
270
+ if len(clean_obs) > 500:
271
+ clean_obs = clean_obs[:500] + "..."
272
+ self._print(f"[dim]💡 {clean_obs}[/dim]")
273
+
274
+ # Add to current step history
275
+ if self.state.steps_history:
276
+ self.state.steps_history[-1]["actions"].append({"type": "observation", "content": observation})
277
+ self.state.steps_history[-1]["status"] = "completed"
278
+
279
+ def _handle_final_answer(self, event: FinalAnswerEvent) -> None:
280
+ """Handle final answer event."""
281
+ answer = event.answer
282
+
283
+ prefix = self._get_display_prefix()
284
+ # Update progress
285
+ self.update_progress(f"{prefix}✅ Finalizing answer...")
286
+
287
+ # Render the answer as markdown
288
+ from rich.markdown import Markdown
289
+
290
+ self._print(Markdown(str(answer)))
291
+
292
+ def _handle_error(self, event: ErrorEvent) -> None:
293
+ """Handle error event."""
294
+ error = event.error
295
+ error_type = event.error_type or "Error"
296
+
297
+ prefix = self._get_display_prefix()
298
+ # Update progress
299
+ self.update_progress(f"{prefix}❌ Error occurred...")
300
+
301
+ # Always show errors prominently (panels removed)
302
+ self._print(f"[bold red]⚠️ {error_type}: {error}[/bold red]")
303
+
304
+ # Add to current step history
305
+ if self.state.steps_history:
306
+ self.state.steps_history[-1]["actions"].append({"type": "error", "content": error})
307
+ self.state.steps_history[-1]["status"] = "error"
308
+
309
+ def _handle_llm_message(self, event: LLMMessageEvent) -> None:
310
+ """Handle LLM reasoning message event."""
311
+ if not self.show_llm_messages:
312
+ return
313
+
314
+ content = event.content
315
+
316
+ if content.strip():
317
+ # If showing code blocks separately, strip them from reasoning to avoid duplication
318
+ if self.show_code:
319
+ content = self._strip_code_blocks(content)
320
+
321
+ # Clean up the content and show as reasoning (panels removed)
322
+ if content.strip():
323
+ self._print(Markdown(content.strip()))
324
+
325
+ @staticmethod
326
+ def _strip_code_blocks(content: str) -> str:
327
+ """Strip markdown code blocks from content.
328
+
329
+ Args:
330
+ content: Text content that may contain markdown code blocks
331
+
332
+ Returns:
333
+ Content with code blocks removed
334
+ """
335
+ import re
336
+
337
+ # Remove fenced code blocks (```...```)
338
+ content = re.sub(r"```[\s\S]*?```", "", content)
339
+ # Remove indented code blocks (4+ spaces at line start)
340
+ content = re.sub(r"(?m)^[ ]{4,}.*$", "", content)
341
+ return content
342
+
343
+ def _handle_reasoning_content(self, event: ReasoningContentEvent) -> None:
344
+ """Handle reasoning content from reasoning models (Claude, Deepseek with exposed reasoning)."""
345
+ content = event.content
346
+ step = event.step
347
+
348
+ if content and content.strip():
349
+ prefix = self._get_display_prefix()
350
+
351
+ # Build title with step number if available
352
+ title_prefix = "🧠 Model Reasoning"
353
+ if step is not None:
354
+ title_prefix = f"🧠 Model Reasoning (Turn {step})"
355
+
356
+ # Update progress
357
+ self.update_progress(f"{prefix}🧠 Processing reasoning content...")
358
+
359
+ # Truncate very long reasoning content for display (panels removed)
360
+ max_length = 2000
361
+ display_content = content.strip()
362
+ if len(display_content) > max_length:
363
+ display_content = display_content[:max_length] + "\n\n[dim]... (truncated)[/dim]"
364
+
365
+ self._print(f"[magenta]{title_prefix}:[/magenta]")
366
+ self._print(f"[dim magenta]{display_content}[/dim magenta]")
367
+
368
+ def _handle_reasoning_tokens(self, event: ReasoningTokensEvent) -> None:
369
+ """Handle reasoning token counts from models like o1/o3 that don't expose reasoning content."""
370
+ tokens = event.tokens
371
+ step = event.step
372
+
373
+ if tokens:
374
+ # Build message with turn number if available (panels removed)
375
+ if step is not None:
376
+ message = f"🧠 Turn {step}: Used {tokens} reasoning tokens"
377
+ else:
378
+ message = f"🧠 Used {tokens} reasoning tokens"
379
+
380
+ self._print(f"[dim magenta]{message}[/dim magenta]")
381
+
382
+ def _build_cost_summary_text(
383
+ self,
384
+ cost: Optional[float],
385
+ total_tokens: Optional[int],
386
+ reasoning_tokens: Optional[int],
387
+ include_emojis: bool = True,
388
+ duration_seconds: Optional[float] = None,
389
+ ) -> Optional[str]:
390
+ """Build cost summary text from metrics.
391
+
392
+ Args:
393
+ cost: Execution cost in dollars
394
+ total_tokens: Total tokens used
395
+ reasoning_tokens: Reasoning tokens used
396
+ include_emojis: Whether to include emoji decorations
397
+ duration_seconds: Execution duration in seconds
398
+
399
+ Returns:
400
+ Formatted summary text or None if no metrics available
401
+ """
402
+ if cost is None and total_tokens is None and duration_seconds is None:
403
+ return None
404
+
405
+ parts = []
406
+
407
+ # Duration (show first if available)
408
+ if duration_seconds is not None:
409
+ duration_prefix = "⏱️ " if include_emojis else ""
410
+ if duration_seconds < 60:
411
+ parts.append(f"{duration_prefix}Duration: {duration_seconds:.1f}s")
412
+ else:
413
+ minutes = int(duration_seconds // 60)
414
+ seconds = duration_seconds % 60
415
+ parts.append(f"{duration_prefix}Duration: {minutes}m {seconds:.1f}s")
416
+
417
+ if cost is not None and cost > 0:
418
+ cost_prefix = "💰 " if include_emojis else ""
419
+ parts.append(f"{cost_prefix}Cost: ${cost:.6f}")
420
+
421
+ if total_tokens is not None:
422
+ token_prefix = "📊 " if include_emojis else ""
423
+ if reasoning_tokens is not None and reasoning_tokens > 0:
424
+ parts.append(f"{token_prefix}Tokens: {total_tokens:,} total ({reasoning_tokens:,} reasoning)")
425
+ else:
426
+ parts.append(f"{token_prefix}Tokens: {total_tokens:,}")
427
+
428
+ if not parts:
429
+ return None
430
+
431
+ return " | ".join(parts)
432
+
433
+ def _handle_cost_summary(self, event: CostSummaryEvent) -> None:
434
+ """Handle cost summary display after final answer."""
435
+ cost = event.cost
436
+ total_tokens = event.tokens
437
+ reasoning_tokens = None
438
+ duration_seconds = event.duration_seconds
439
+ cached_tokens = event.cached_tokens
440
+ cache_creation_tokens = event.cache_creation_input_tokens
441
+ cache_read_tokens = event.cache_read_input_tokens
442
+
443
+ summary_text = self._build_cost_summary_text(
444
+ cost, total_tokens, reasoning_tokens, duration_seconds=duration_seconds
445
+ )
446
+ if not summary_text:
447
+ return
448
+
449
+ # Add cache statistics if available
450
+ cache_parts = []
451
+ if cached_tokens and cached_tokens > 0:
452
+ cache_parts.append(f"💾 Cached: {cached_tokens:,} tokens")
453
+ if cache_creation_tokens and cache_creation_tokens > 0:
454
+ cache_parts.append(f"📝 Cache write: {cache_creation_tokens:,} tokens")
455
+ if cache_read_tokens and cache_read_tokens > 0:
456
+ cache_parts.append(f"📖 Cache read: {cache_read_tokens:,} tokens")
457
+
458
+ if cache_parts:
459
+ cache_summary = " | ".join(cache_parts)
460
+ if self.show_panels:
461
+ self._print(Text(summary_text, style="dim cyan"))
462
+ self._print(Text(cache_summary, style="dim green"))
463
+ else:
464
+ self._print(f"[dim cyan]{summary_text}[/dim cyan]")
465
+ self._print(f"[dim green]{cache_summary}[/dim green]")
466
+ else:
467
+ if self.show_panels:
468
+ self._print(Text(summary_text, style="dim cyan"))
469
+ else:
470
+ self._print(f"[dim cyan]{summary_text}[/dim cyan]")
471
+
472
+ def _handle_execution_result(self, event: ExecutionResultEvent) -> None:
473
+ """Handle code execution result event."""
474
+ if not self.show_execution_results:
475
+ return
476
+
477
+ prefix = self._get_display_prefix()
478
+ # Update progress
479
+ self.update_progress(f"{prefix}📊 Processing execution results...")
480
+
481
+ # Display execution logs if present (always show with execution results)
482
+ if event.logs:
483
+ logs_text = "\n".join(event.logs)
484
+ if logs_text.strip():
485
+ self._print(f"[dim]📝 {logs_text}[/dim]")
486
+
487
+ # Display output if present and meaningful
488
+ if event.output:
489
+ output_text = event.output
490
+ contains_error = self._contains_error(output_text)
491
+
492
+ # Check if this is a final answer
493
+ is_final_answer = "__FINAL_ANSWER__:" in output_text
494
+
495
+ # Always show errors, filter non-meaningful outputs otherwise
496
+ if contains_error:
497
+ # Show errors prominently
498
+ self._print(f"[red]📤 Output (Error): {output_text}[/red]")
499
+ elif is_final_answer:
500
+ # Skip displaying final answer here - it will be displayed by _handle_final_answer event
501
+ pass
502
+ elif output_text.strip() and output_text.strip().lower() not in ("none", "null", ""):
503
+ # Show normal meaningful output
504
+ self._print(f"[bold cyan]📤 Output:[/bold cyan] {output_text}")
505
+
506
+ def _handle_execution_logs(self, event: ExecutionLogsEvent) -> None:
507
+ """Handle execution logs event."""
508
+ if not self.show_execution_logs:
509
+ return
510
+
511
+ content = event.logs
512
+
513
+ if content.strip() and "Execution logs:" in content:
514
+ # Extract just the log content
515
+ logs = content.replace("Execution logs:", "").strip()
516
+ if logs:
517
+ self._print(f"[dim]📝 {logs}[/dim]")
518
+
519
+ def _handle_stream_chunk(self, event: StreamChunkEvent) -> None:
520
+ """Handle streaming chunk event."""
521
+ chunk = event.chunk
522
+ self.streaming_content += chunk
523
+ self.is_streaming = True
524
+
525
+ prefix = self._get_display_prefix()
526
+ # Update progress to show streaming
527
+ self.update_progress(f"{prefix}💬 Streaming response...")
528
+
529
+ # Print the chunk directly for real-time feedback
530
+ self._print(chunk, end="", highlight=False)
531
+
532
+ def _handle_stream_complete(self, event: StreamCompleteEvent) -> None:
533
+ """Handle streaming complete event."""
534
+ self.is_streaming = False
535
+
536
+ # Print newline after streaming completes
537
+ self._print()
538
+
539
+ prefix = self._get_display_prefix()
540
+ # Update progress
541
+ self.update_progress(f"{prefix}✅ Streaming complete")
542
+
543
+ # Clear streaming content for next step
544
+ self.streaming_content = ""
545
+
546
+ def _handle_info(self, event: InfoEvent) -> None:
547
+ """Handle info event for informational messages."""
548
+ message = event.message
549
+ if message:
550
+ self._print(f"[dim]{message}[/dim]")
551
+
552
+ def _handle_debug_message(self, event: DebugMessageEvent) -> None:
553
+ """Handle debug message event."""
554
+ if not self.show_debug_messages:
555
+ return
556
+ message = event.message
557
+ if message:
558
+ self._print(f"[dim blue]{message}[/dim blue]")
559
+
560
+ def _handle_warning(self, event: WarningEvent) -> None:
561
+ """Handle warning event."""
562
+ message = event.message
563
+ if message:
564
+ self._print(f"[yellow]{message}[/yellow]")
565
+
566
+ def _handle_step_progress(self, event: StepProgressEvent) -> None:
567
+ """Handle step progress event."""
568
+ message = event.message
569
+ if message:
570
+ prefix = self._get_display_prefix()
571
+ self._print(f"[cyan]{prefix}{message}[/cyan]")
572
+
573
+ def _update_display(self) -> None:
574
+ """Update the live display with current state."""
575
+ # This could be enhanced with a live progress display
576
+ # For now, we use discrete updates
577
+ pass
578
+
579
+ @contextmanager
580
+ def progress_context(self) -> Generator[None, None, None]:
581
+ """Context manager for showing progress during execution."""
582
+ self.progress = Progress(
583
+ SpinnerColumn(),
584
+ TextColumn("[bold blue]{task.description}", justify="left"),
585
+ transient=True, # Auto-clear progress when done for cleaner output
586
+ console=self.console,
587
+ refresh_per_second=20, # Higher refresh rate for real-time updates
588
+ )
589
+
590
+ # Store console, progress, and ui_handler in thread-local for tool access
591
+ set_ui_context(console=self.console, progress=self.progress, ui_handler=self)
592
+
593
+ with self.progress:
594
+ self.task_id = self.progress.add_task("Starting agent...", total=None)
595
+ try:
596
+ yield
597
+ finally:
598
+ self.progress.stop()
599
+ clear_ui_context()
600
+
601
+ def update_progress(self, description: str) -> None:
602
+ """Update progress description."""
603
+ if self.progress and self.task_id is not None:
604
+ self.progress.update(self.task_id, description=description)
605
+
606
+ @contextmanager
607
+ def pause_for_input(self) -> Generator[None, None, None]:
608
+ """Pause the progress display for user input.
609
+
610
+ Stops the progress, shows the prompt, then restarts it.
611
+ The transient=True flag ensures clean redraw when restarted.
612
+ """
613
+ if self.progress is not None:
614
+ self.progress.stop()
615
+
616
+ try:
617
+ yield
618
+ finally:
619
+ if self.progress is not None:
620
+ self.progress.start()
621
+
622
+ def set_multistep_context(self, step_number: int, step_name: str, total_steps: int) -> None:
623
+ """Set multi-step execution context.
624
+
625
+ Args:
626
+ step_number: Current multi-step number (1-indexed)
627
+ step_name: Name of current multi-step
628
+ total_steps: Total number of multi-steps
629
+ """
630
+ self.state.multistep_context = {
631
+ "step_number": step_number,
632
+ "step_name": step_name,
633
+ "total_steps": total_steps,
634
+ }
635
+
636
+ def clear_multistep_context(self) -> None:
637
+ """Clear multi-step execution context."""
638
+ self.state.multistep_context = None