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/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
|