stirrup 0.1.0__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.
- stirrup/__init__.py +76 -0
- stirrup/clients/__init__.py +14 -0
- stirrup/clients/chat_completions_client.py +219 -0
- stirrup/clients/litellm_client.py +141 -0
- stirrup/clients/utils.py +161 -0
- stirrup/constants.py +14 -0
- stirrup/core/__init__.py +1 -0
- stirrup/core/agent.py +1097 -0
- stirrup/core/exceptions.py +7 -0
- stirrup/core/models.py +599 -0
- stirrup/prompts/__init__.py +22 -0
- stirrup/prompts/base_system_prompt.txt +1 -0
- stirrup/prompts/message_summarizer.txt +27 -0
- stirrup/prompts/message_summarizer_bridge.txt +11 -0
- stirrup/py.typed +0 -0
- stirrup/tools/__init__.py +77 -0
- stirrup/tools/calculator.py +32 -0
- stirrup/tools/code_backends/__init__.py +38 -0
- stirrup/tools/code_backends/base.py +454 -0
- stirrup/tools/code_backends/docker.py +752 -0
- stirrup/tools/code_backends/e2b.py +359 -0
- stirrup/tools/code_backends/local.py +481 -0
- stirrup/tools/finish.py +23 -0
- stirrup/tools/mcp.py +500 -0
- stirrup/tools/view_image.py +83 -0
- stirrup/tools/web.py +336 -0
- stirrup/utils/__init__.py +10 -0
- stirrup/utils/logging.py +944 -0
- stirrup/utils/text.py +11 -0
- stirrup-0.1.0.dist-info/METADATA +318 -0
- stirrup-0.1.0.dist-info/RECORD +32 -0
- stirrup-0.1.0.dist-info/WHEEL +4 -0
stirrup/utils/logging.py
ADDED
|
@@ -0,0 +1,944 @@
|
|
|
1
|
+
"""Rich logging for agent workflows with visual hierarchy."""
|
|
2
|
+
|
|
3
|
+
import html
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Self, cast
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
from rich import box
|
|
12
|
+
from rich.console import Console, RenderableType
|
|
13
|
+
from rich.live import Live
|
|
14
|
+
from rich.logging import RichHandler
|
|
15
|
+
from rich.markdown import Markdown
|
|
16
|
+
from rich.padding import Padding
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
from rich.rule import Rule
|
|
19
|
+
from rich.spinner import Spinner
|
|
20
|
+
from rich.syntax import Syntax
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
from rich.text import Text
|
|
23
|
+
from rich.tree import Tree
|
|
24
|
+
|
|
25
|
+
from stirrup.core.models import AssistantMessage, ToolMessage, UserMessage, _aggregate_list, aggregate_metadata
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"AgentLogger",
|
|
29
|
+
"AgentLoggerBase",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Shared console instance
|
|
33
|
+
console = Console()
|
|
34
|
+
|
|
35
|
+
# Indentation spaces per sub-agent nesting level
|
|
36
|
+
SUBAGENT_INDENT_SPACES: int = 8
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _is_subagent_metadata(data: object) -> bool:
|
|
40
|
+
"""Check if data represents sub-agent metadata.
|
|
41
|
+
|
|
42
|
+
Sub-agent metadata can be:
|
|
43
|
+
- A Pydantic SubAgentMetadata object with run_metadata attribute
|
|
44
|
+
- A dict where all values are dicts/objects (from aggregate_metadata flattening)
|
|
45
|
+
"""
|
|
46
|
+
# Check for Pydantic SubAgentMetadata object
|
|
47
|
+
if hasattr(data, "run_metadata") and isinstance(data.run_metadata, dict):
|
|
48
|
+
return True
|
|
49
|
+
# Check for flattened dict of dicts (from aggregate_metadata)
|
|
50
|
+
if isinstance(data, dict) and data:
|
|
51
|
+
return all(isinstance(v, dict) or hasattr(v, "model_dump") for v in data.values())
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _format_token_usage(data: object) -> str:
|
|
56
|
+
"""Format token_usage (dict or TokenUsage object) as a human-readable string."""
|
|
57
|
+
if isinstance(data, dict):
|
|
58
|
+
# Dict representation
|
|
59
|
+
data_dict = cast(dict[str, Any], data)
|
|
60
|
+
input_tokens: int = data_dict.get("input", 0)
|
|
61
|
+
output_tokens: int = data_dict.get("output", 0)
|
|
62
|
+
reasoning_tokens: int = data_dict.get("reasoning", 0)
|
|
63
|
+
elif hasattr(data, "input") and hasattr(data, "output"):
|
|
64
|
+
# Pydantic TokenUsage object - use getattr for type safety
|
|
65
|
+
input_tokens = int(getattr(data, "input", 0))
|
|
66
|
+
output_tokens = int(getattr(data, "output", 0))
|
|
67
|
+
reasoning_tokens = int(getattr(data, "reasoning", 0))
|
|
68
|
+
else:
|
|
69
|
+
return str(data)
|
|
70
|
+
total = input_tokens + output_tokens + reasoning_tokens
|
|
71
|
+
return f"{total:,} tokens"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _get_nested_tools(data: object) -> dict[str, object]:
|
|
75
|
+
"""Extract nested tools dict from sub-agent metadata."""
|
|
76
|
+
if hasattr(data, "run_metadata"):
|
|
77
|
+
# Pydantic SubAgentMetadata - return its run_metadata
|
|
78
|
+
run_metadata = data.run_metadata
|
|
79
|
+
if isinstance(run_metadata, dict):
|
|
80
|
+
return cast(dict[str, object], run_metadata)
|
|
81
|
+
if isinstance(data, dict):
|
|
82
|
+
# Already a dict
|
|
83
|
+
return cast(dict[str, object], data)
|
|
84
|
+
return {}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _add_tool_branch(
|
|
88
|
+
parent: Tree,
|
|
89
|
+
tool_name: str,
|
|
90
|
+
tool_data: object,
|
|
91
|
+
skip_fields: set[str],
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Add a tool entry to the tree, handling nested sub-agent data recursively.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
parent: The tree or branch to add to
|
|
97
|
+
tool_name: Name of the tool or sub-agent
|
|
98
|
+
tool_data: The tool's metadata (dict, Pydantic model, list, or scalar)
|
|
99
|
+
skip_fields: Fields to skip when displaying dict contents
|
|
100
|
+
"""
|
|
101
|
+
# Special case: token_usage formatted as total tokens
|
|
102
|
+
if tool_name == "token_usage":
|
|
103
|
+
if isinstance(tool_data, list) and tool_data:
|
|
104
|
+
parent.add(f"[dim]token_usage:[/] {_format_token_usage(tool_data[0])}")
|
|
105
|
+
else:
|
|
106
|
+
parent.add(f"[dim]token_usage:[/] {_format_token_usage(tool_data)}")
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# Case 1: List → aggregate using __add__, then recurse
|
|
110
|
+
if isinstance(tool_data, list) and tool_data:
|
|
111
|
+
aggregated = _aggregate_list(tool_data)
|
|
112
|
+
if aggregated is not None:
|
|
113
|
+
_add_tool_branch(parent, tool_name, aggregated, skip_fields)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Case 2: SubAgentMetadata → recurse into run_metadata only
|
|
117
|
+
if _is_subagent_metadata(tool_data):
|
|
118
|
+
branch = parent.add(f"[magenta]{tool_name}[/]")
|
|
119
|
+
for nested_name, nested_data in sorted(_get_nested_tools(tool_data).items()):
|
|
120
|
+
_add_tool_branch(branch, nested_name, nested_data, skip_fields)
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Case 3: Leaf node - display fields as branches
|
|
124
|
+
# Convert to dict if Pydantic model
|
|
125
|
+
if hasattr(tool_data, "model_dump"):
|
|
126
|
+
data_dict = cast(Callable[[], dict[str, Any]], tool_data.model_dump)()
|
|
127
|
+
elif isinstance(tool_data, dict):
|
|
128
|
+
data_dict = cast(dict[str, Any], tool_data)
|
|
129
|
+
else:
|
|
130
|
+
# Scalar value - just show it inline
|
|
131
|
+
parent.add(f"[magenta]{tool_name}[/]: {tool_data}")
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
# Show num_uses inline with the tool name if present
|
|
135
|
+
num_uses = data_dict.get("num_uses")
|
|
136
|
+
if num_uses is not None:
|
|
137
|
+
branch = parent.add(f"[magenta]{tool_name}[/]: {num_uses} call(s)")
|
|
138
|
+
else:
|
|
139
|
+
branch = parent.add(f"[magenta]{tool_name}[/]")
|
|
140
|
+
|
|
141
|
+
for k, v in data_dict.items():
|
|
142
|
+
if k not in skip_fields and v is not None:
|
|
143
|
+
branch.add(f"[dim]{k}:[/] {v}")
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class AgentLoggerBase(ABC):
|
|
147
|
+
"""Abstract base class for agent loggers.
|
|
148
|
+
|
|
149
|
+
Defines the interface that Agent uses for logging. Implement this to create
|
|
150
|
+
custom loggers (e.g., for testing, file output, or monitoring services).
|
|
151
|
+
|
|
152
|
+
Properties are set by Agent after construction:
|
|
153
|
+
- name, model, max_turns, depth: Agent configuration
|
|
154
|
+
- finish_params, run_metadata, output_dir: Set before __exit__ for final stats
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
# Properties set by Agent after construction
|
|
158
|
+
name: str
|
|
159
|
+
model: str | None
|
|
160
|
+
max_turns: int | None
|
|
161
|
+
depth: int
|
|
162
|
+
|
|
163
|
+
# State updated during run (set before __exit__)
|
|
164
|
+
finish_params: BaseModel | None
|
|
165
|
+
run_metadata: dict[str, list[Any]] | None
|
|
166
|
+
output_dir: str | None
|
|
167
|
+
|
|
168
|
+
@abstractmethod
|
|
169
|
+
def __enter__(self) -> Self:
|
|
170
|
+
"""Enter logging context. Called when agent session starts."""
|
|
171
|
+
...
|
|
172
|
+
|
|
173
|
+
@abstractmethod
|
|
174
|
+
def __exit__(
|
|
175
|
+
self,
|
|
176
|
+
exc_type: type[BaseException] | None,
|
|
177
|
+
exc_val: BaseException | None,
|
|
178
|
+
exc_tb: object,
|
|
179
|
+
) -> None:
|
|
180
|
+
"""Exit logging context. Called when agent session ends."""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
@abstractmethod
|
|
184
|
+
def on_step(
|
|
185
|
+
self,
|
|
186
|
+
step: int,
|
|
187
|
+
tool_calls: int = 0,
|
|
188
|
+
input_tokens: int = 0,
|
|
189
|
+
output_tokens: int = 0,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""Report step progress and stats during agent execution."""
|
|
192
|
+
...
|
|
193
|
+
|
|
194
|
+
@abstractmethod
|
|
195
|
+
def assistant_message(
|
|
196
|
+
self,
|
|
197
|
+
turn: int,
|
|
198
|
+
max_turns: int,
|
|
199
|
+
assistant_message: AssistantMessage,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""Log an assistant message."""
|
|
202
|
+
...
|
|
203
|
+
|
|
204
|
+
@abstractmethod
|
|
205
|
+
def user_message(self, user_message: UserMessage) -> None:
|
|
206
|
+
"""Log a user message."""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
@abstractmethod
|
|
210
|
+
def task_message(self, task: str | list[Any]) -> None:
|
|
211
|
+
"""Log the initial task/prompt at the start of a run."""
|
|
212
|
+
...
|
|
213
|
+
|
|
214
|
+
@abstractmethod
|
|
215
|
+
def tool_result(self, tool_message: ToolMessage) -> None:
|
|
216
|
+
"""Log a tool execution result."""
|
|
217
|
+
...
|
|
218
|
+
|
|
219
|
+
@abstractmethod
|
|
220
|
+
def context_summarization_start(self, pct_used: float, cutoff: float) -> None:
|
|
221
|
+
"""Log that context summarization is starting."""
|
|
222
|
+
...
|
|
223
|
+
|
|
224
|
+
@abstractmethod
|
|
225
|
+
def context_summarization_complete(self, summary: str, bridge: str) -> None:
|
|
226
|
+
"""Log completed context summarization."""
|
|
227
|
+
...
|
|
228
|
+
|
|
229
|
+
# Standard logging methods
|
|
230
|
+
@abstractmethod
|
|
231
|
+
def debug(self, message: str, *args: object) -> None:
|
|
232
|
+
"""Log a debug message."""
|
|
233
|
+
...
|
|
234
|
+
|
|
235
|
+
@abstractmethod
|
|
236
|
+
def info(self, message: str, *args: object) -> None:
|
|
237
|
+
"""Log an info message."""
|
|
238
|
+
...
|
|
239
|
+
|
|
240
|
+
@abstractmethod
|
|
241
|
+
def warning(self, message: str, *args: object) -> None:
|
|
242
|
+
"""Log a warning message."""
|
|
243
|
+
...
|
|
244
|
+
|
|
245
|
+
@abstractmethod
|
|
246
|
+
def error(self, message: str, *args: object) -> None:
|
|
247
|
+
"""Log an error message."""
|
|
248
|
+
...
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class AgentLogger(AgentLoggerBase):
|
|
252
|
+
"""Rich console logger for agent workflows.
|
|
253
|
+
|
|
254
|
+
Implements AgentLoggerBase with rich formatting, spinners, and visual hierarchy.
|
|
255
|
+
Each agent (including sub-agents) should have its own logger instance.
|
|
256
|
+
|
|
257
|
+
Usage:
|
|
258
|
+
from stirrup.clients.chat_completions_client import ChatCompletionsClient
|
|
259
|
+
|
|
260
|
+
# Agent creates logger internally by default
|
|
261
|
+
client = ChatCompletionsClient(model="gpt-4")
|
|
262
|
+
agent = Agent(client=client, name="assistant")
|
|
263
|
+
|
|
264
|
+
# Or pass a pre-configured logger
|
|
265
|
+
logger = AgentLogger(show_spinner=False)
|
|
266
|
+
agent = Agent(client=client, name="assistant", logger=logger)
|
|
267
|
+
|
|
268
|
+
# Agent sets these properties before calling __enter__:
|
|
269
|
+
# logger.name, logger.model, logger.max_turns, logger.depth
|
|
270
|
+
|
|
271
|
+
# Agent sets these before calling __exit__:
|
|
272
|
+
# logger.finish_params, logger.run_metadata, logger.output_dir
|
|
273
|
+
"""
|
|
274
|
+
|
|
275
|
+
def __init__(
|
|
276
|
+
self,
|
|
277
|
+
*,
|
|
278
|
+
show_spinner: bool = True,
|
|
279
|
+
level: int = logging.INFO,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""Initialize the agent logger.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
show_spinner: Whether to show a spinner while agent runs (only for depth=0)
|
|
285
|
+
level: Logging level (default: INFO)
|
|
286
|
+
"""
|
|
287
|
+
# Properties set by Agent before __enter__
|
|
288
|
+
self.name: str = "agent"
|
|
289
|
+
self.model: str | None = None
|
|
290
|
+
self.max_turns: int | None = None
|
|
291
|
+
self.depth: int = 0
|
|
292
|
+
|
|
293
|
+
# State set by Agent before __exit__
|
|
294
|
+
self.finish_params: BaseModel | None = None
|
|
295
|
+
self.run_metadata: dict[str, list[Any]] | None = None
|
|
296
|
+
self.output_dir: str | None = None
|
|
297
|
+
|
|
298
|
+
# Configuration
|
|
299
|
+
self._show_spinner = show_spinner
|
|
300
|
+
self._level = level
|
|
301
|
+
|
|
302
|
+
# Spinner state (only used when depth == 0 and show_spinner is True)
|
|
303
|
+
self._current_step = 0
|
|
304
|
+
self._tool_calls = 0
|
|
305
|
+
self._input_tokens = 0
|
|
306
|
+
self._output_tokens = 0
|
|
307
|
+
self._live: Live | None = None
|
|
308
|
+
|
|
309
|
+
# Configure rich logging on first logger creation
|
|
310
|
+
self._configure_logging()
|
|
311
|
+
|
|
312
|
+
def _configure_logging(self) -> None:
|
|
313
|
+
"""Configure rich logging with agent-aware formatting."""
|
|
314
|
+
handler = RichHandler(
|
|
315
|
+
console=console,
|
|
316
|
+
show_time=True,
|
|
317
|
+
show_path=False,
|
|
318
|
+
rich_tracebacks=True,
|
|
319
|
+
markup=True,
|
|
320
|
+
show_level=False,
|
|
321
|
+
)
|
|
322
|
+
handler.setFormatter(logging.Formatter("%(message)s"))
|
|
323
|
+
|
|
324
|
+
root = logging.getLogger()
|
|
325
|
+
root.handlers.clear()
|
|
326
|
+
root.addHandler(handler)
|
|
327
|
+
root.setLevel(self._level)
|
|
328
|
+
|
|
329
|
+
# Silence noisy loggers
|
|
330
|
+
for name in [
|
|
331
|
+
"LiteLLM",
|
|
332
|
+
"httpx",
|
|
333
|
+
"httpcore",
|
|
334
|
+
"openai",
|
|
335
|
+
"utils",
|
|
336
|
+
"asyncio",
|
|
337
|
+
"filelock",
|
|
338
|
+
"fsspec",
|
|
339
|
+
"urllib3",
|
|
340
|
+
"markdown_it",
|
|
341
|
+
"docker",
|
|
342
|
+
"e2b",
|
|
343
|
+
"e2b.api",
|
|
344
|
+
"e2b.sandbox_async",
|
|
345
|
+
"e2b.sandbox_sync",
|
|
346
|
+
]:
|
|
347
|
+
logging.getLogger(name).setLevel(logging.WARNING)
|
|
348
|
+
|
|
349
|
+
# These loggers need ERROR level to fully suppress verbose debug output
|
|
350
|
+
for name in [
|
|
351
|
+
"mcp",
|
|
352
|
+
"mcp.client",
|
|
353
|
+
"mcp.client.sse",
|
|
354
|
+
"mcp.client.streamable_http",
|
|
355
|
+
"httpx_sse",
|
|
356
|
+
"readability",
|
|
357
|
+
"readability.readability",
|
|
358
|
+
"trafilatura",
|
|
359
|
+
"trafilatura.core",
|
|
360
|
+
"trafilatura.readability_lxml",
|
|
361
|
+
"htmldate",
|
|
362
|
+
"courlan",
|
|
363
|
+
]:
|
|
364
|
+
logging.getLogger(name).setLevel(logging.ERROR)
|
|
365
|
+
|
|
366
|
+
def _get_indent(self) -> str:
|
|
367
|
+
"""Get indentation string based on current agent depth."""
|
|
368
|
+
return "│ " * self.depth
|
|
369
|
+
|
|
370
|
+
def _print_indented(self, renderable: RenderableType, indent: str | int) -> None:
|
|
371
|
+
"""Print a renderable with indentation using Padding.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
renderable: The Rich renderable to print (Panel, Tree, Table, Rule, etc.)
|
|
375
|
+
indent: Either a string prefix or number of spaces for left padding
|
|
376
|
+
"""
|
|
377
|
+
if isinstance(indent, str):
|
|
378
|
+
# For string indents, use capture method
|
|
379
|
+
with console.capture() as capture:
|
|
380
|
+
console.print(renderable)
|
|
381
|
+
output = capture.get()
|
|
382
|
+
for line in output.rstrip("\n").split("\n"):
|
|
383
|
+
console.print(f"{indent}{line}")
|
|
384
|
+
else:
|
|
385
|
+
# For numeric indents, use Padding
|
|
386
|
+
console.print(Padding(renderable, (0, 0, 0, indent)))
|
|
387
|
+
|
|
388
|
+
def _make_spinner_text(self) -> Text:
|
|
389
|
+
"""Create styled text for the spinner display."""
|
|
390
|
+
text = Text()
|
|
391
|
+
|
|
392
|
+
text.append("Running ", style="bold green")
|
|
393
|
+
text.append(self.name, style="bold green")
|
|
394
|
+
|
|
395
|
+
# Separator
|
|
396
|
+
text.append(" │ ", style="dim")
|
|
397
|
+
|
|
398
|
+
# Step count
|
|
399
|
+
if self.max_turns:
|
|
400
|
+
text.append(f"{self._current_step}/{self.max_turns}", style="cyan bold")
|
|
401
|
+
text.append(" steps", style="cyan")
|
|
402
|
+
else:
|
|
403
|
+
text.append(f"{self._current_step}", style="cyan bold")
|
|
404
|
+
text.append(" steps", style="cyan")
|
|
405
|
+
|
|
406
|
+
# Separator
|
|
407
|
+
text.append(" │ ", style="dim")
|
|
408
|
+
|
|
409
|
+
# Tool calls
|
|
410
|
+
text.append(f"{self._tool_calls}", style="magenta bold")
|
|
411
|
+
text.append(" tool calls", style="magenta")
|
|
412
|
+
|
|
413
|
+
# Separator
|
|
414
|
+
text.append(" │ ", style="dim")
|
|
415
|
+
|
|
416
|
+
# Input tokens
|
|
417
|
+
text.append(f"{self._input_tokens:,}", style="yellow bold")
|
|
418
|
+
text.append(" input tokens", style="yellow")
|
|
419
|
+
|
|
420
|
+
# Separator
|
|
421
|
+
text.append(" │ ", style="dim")
|
|
422
|
+
|
|
423
|
+
# Output tokens
|
|
424
|
+
text.append(f"{self._output_tokens:,}", style="blue bold")
|
|
425
|
+
text.append(" output tokens", style="blue")
|
|
426
|
+
|
|
427
|
+
return text
|
|
428
|
+
|
|
429
|
+
def _make_spinner(self) -> Spinner:
|
|
430
|
+
"""Create spinner with current stats."""
|
|
431
|
+
return Spinner("aesthetic", text=self._make_spinner_text(), style="green")
|
|
432
|
+
|
|
433
|
+
# -------------------------------------------------------------------------
|
|
434
|
+
# Context Manager Methods (AgentLoggerBase implementation)
|
|
435
|
+
# -------------------------------------------------------------------------
|
|
436
|
+
|
|
437
|
+
def __enter__(self) -> Self:
|
|
438
|
+
"""Enter logging context. Logs agent start and starts spinner if depth=0."""
|
|
439
|
+
# Log agent start (rule + system prompt display)
|
|
440
|
+
indent_spaces = self.depth * SUBAGENT_INDENT_SPACES
|
|
441
|
+
|
|
442
|
+
# Build title with optional model info
|
|
443
|
+
model_str = f" ({self.model})" if self.model else ""
|
|
444
|
+
if self.depth == 0:
|
|
445
|
+
title = f"▶ {self.name}{model_str}"
|
|
446
|
+
console.rule(f"[bold cyan]{title}[/]", style="cyan")
|
|
447
|
+
else:
|
|
448
|
+
title = f"▶ {self.name}: Level {self.depth}{model_str}"
|
|
449
|
+
rule = Rule(f"[bold cyan]{title}[/]", style="cyan")
|
|
450
|
+
self._print_indented(rule, indent_spaces)
|
|
451
|
+
console.print()
|
|
452
|
+
|
|
453
|
+
# Start spinner only for top-level agent
|
|
454
|
+
if self.depth == 0 and self._show_spinner:
|
|
455
|
+
self._live = Live(self._make_spinner(), console=console, refresh_per_second=10)
|
|
456
|
+
self._live.start()
|
|
457
|
+
|
|
458
|
+
return self
|
|
459
|
+
|
|
460
|
+
def __exit__(
|
|
461
|
+
self,
|
|
462
|
+
exc_type: type[BaseException] | None,
|
|
463
|
+
exc_val: BaseException | None,
|
|
464
|
+
exc_tb: object,
|
|
465
|
+
) -> None:
|
|
466
|
+
"""Exit logging context. Stops spinner and logs completion stats."""
|
|
467
|
+
# Stop spinner first
|
|
468
|
+
if self._live:
|
|
469
|
+
self._live.stop()
|
|
470
|
+
self._live = None
|
|
471
|
+
|
|
472
|
+
error = str(exc_val) if exc_type is not None else None
|
|
473
|
+
self._log_finish(error=error)
|
|
474
|
+
|
|
475
|
+
def _log_finish(self, error: str | None = None) -> None:
|
|
476
|
+
"""Log agent completion with full statistics."""
|
|
477
|
+
console.print() # Add spacing before finish
|
|
478
|
+
|
|
479
|
+
# Determine status
|
|
480
|
+
if error:
|
|
481
|
+
status = f"[bold red]✗ {self.name} - Error[/]"
|
|
482
|
+
style = "red"
|
|
483
|
+
elif self.finish_params is None:
|
|
484
|
+
# Agent didn't call finish tool (e.g., ran out of turns)
|
|
485
|
+
status = f"[bold red]✗ {self.name} - Failed[/]"
|
|
486
|
+
style = "red"
|
|
487
|
+
else:
|
|
488
|
+
status = f"[bold green]✓ {self.name} - Complete[/]"
|
|
489
|
+
style = "green"
|
|
490
|
+
|
|
491
|
+
indent_spaces = self.depth * SUBAGENT_INDENT_SPACES
|
|
492
|
+
if self.depth == 0:
|
|
493
|
+
console.rule(status, style=style)
|
|
494
|
+
else:
|
|
495
|
+
rule = Rule(status, style=style)
|
|
496
|
+
self._print_indented(rule, indent_spaces)
|
|
497
|
+
|
|
498
|
+
# Display error if present
|
|
499
|
+
if error:
|
|
500
|
+
error_text = Text(f"Error: {error}", style="red")
|
|
501
|
+
if self.depth > 0:
|
|
502
|
+
self._print_indented(error_text, indent_spaces)
|
|
503
|
+
else:
|
|
504
|
+
console.print(error_text)
|
|
505
|
+
console.print()
|
|
506
|
+
|
|
507
|
+
# For subagents, only show the status rule (and error if present)
|
|
508
|
+
if self.depth > 0:
|
|
509
|
+
return
|
|
510
|
+
|
|
511
|
+
# Extract paths from finish_params for use in metadata section
|
|
512
|
+
paths = None
|
|
513
|
+
if self.finish_params:
|
|
514
|
+
params = self.finish_params.model_dump()
|
|
515
|
+
reason = params.get("reason", "")
|
|
516
|
+
paths = params.get("paths")
|
|
517
|
+
|
|
518
|
+
# Reason panel as markdown (full width)
|
|
519
|
+
reason_panel = Panel(
|
|
520
|
+
Markdown(reason) if reason else "[dim]No reason provided[/]",
|
|
521
|
+
title="[bold]Reason[/]",
|
|
522
|
+
title_align="left",
|
|
523
|
+
border_style="cyan",
|
|
524
|
+
expand=True,
|
|
525
|
+
)
|
|
526
|
+
console.print(reason_panel)
|
|
527
|
+
console.print()
|
|
528
|
+
|
|
529
|
+
# Display run metadata statistics and paths in 1:1:1 layout
|
|
530
|
+
if self.run_metadata or paths:
|
|
531
|
+
# Aggregate metadata to roll up sub-agent token usage into the total
|
|
532
|
+
if self.run_metadata:
|
|
533
|
+
aggregated = aggregate_metadata(self.run_metadata, return_json_serializable=False)
|
|
534
|
+
token_usage_list = aggregated.get("token_usage", [])
|
|
535
|
+
else:
|
|
536
|
+
token_usage_list = []
|
|
537
|
+
tool_keys = (
|
|
538
|
+
[k for k in self.run_metadata if k not in ("token_usage", "finish")] if self.run_metadata else []
|
|
539
|
+
)
|
|
540
|
+
|
|
541
|
+
# Build tool usage tree
|
|
542
|
+
tool_panel = None
|
|
543
|
+
if tool_keys and self.run_metadata:
|
|
544
|
+
tool_tree = Tree("🔧 [bold]Tools[/]", guide_style="dim")
|
|
545
|
+
skip_fields = {"num_uses"}
|
|
546
|
+
for tool_name in sorted(tool_keys):
|
|
547
|
+
_add_tool_branch(tool_tree, tool_name, self.run_metadata[tool_name], skip_fields)
|
|
548
|
+
|
|
549
|
+
tool_panel = Panel(
|
|
550
|
+
tool_tree,
|
|
551
|
+
title="[bold]Tool Usage[/]",
|
|
552
|
+
title_align="left",
|
|
553
|
+
border_style="magenta",
|
|
554
|
+
expand=True,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Build paths panel
|
|
558
|
+
paths_panel = None
|
|
559
|
+
if paths:
|
|
560
|
+
paths_tree = Tree("📁 [bold]Files[/]", guide_style="dim")
|
|
561
|
+
# If output_dir is provided, add it as a parent node
|
|
562
|
+
if self.output_dir:
|
|
563
|
+
output_branch = paths_tree.add(f"[magenta]{self.output_dir}/[/]")
|
|
564
|
+
for path in paths:
|
|
565
|
+
output_branch.add(f"[green]{path}[/]")
|
|
566
|
+
else:
|
|
567
|
+
for path in paths:
|
|
568
|
+
paths_tree.add(f"[green]{path}[/]")
|
|
569
|
+
|
|
570
|
+
paths_panel = Panel(
|
|
571
|
+
paths_tree,
|
|
572
|
+
title="[bold]Paths[/]",
|
|
573
|
+
title_align="left",
|
|
574
|
+
border_style="cyan",
|
|
575
|
+
expand=True,
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# Build token usage table
|
|
579
|
+
token_panel = None
|
|
580
|
+
if token_usage_list:
|
|
581
|
+
total_input = sum(getattr(u, "input", 0) for u in token_usage_list)
|
|
582
|
+
total_output = sum(getattr(u, "output", 0) for u in token_usage_list)
|
|
583
|
+
total_reasoning = sum(getattr(u, "reasoning", 0) for u in token_usage_list)
|
|
584
|
+
total_tokens = sum(getattr(u, "total", 0) for u in token_usage_list)
|
|
585
|
+
|
|
586
|
+
token_table = Table(
|
|
587
|
+
box=box.SIMPLE,
|
|
588
|
+
show_header=True,
|
|
589
|
+
header_style="bold",
|
|
590
|
+
show_footer=True,
|
|
591
|
+
expand=True,
|
|
592
|
+
)
|
|
593
|
+
token_table.add_column("Type", style="cyan", footer="[bold]Total[/]")
|
|
594
|
+
token_table.add_column("Count", justify="right", style="green", footer=f"[bold]{total_tokens:,}[/]")
|
|
595
|
+
|
|
596
|
+
token_table.add_row("Input", f"{total_input:,}")
|
|
597
|
+
token_table.add_row("Output", f"{total_output:,}")
|
|
598
|
+
if total_reasoning > 0:
|
|
599
|
+
token_table.add_row("Reasoning", f"{total_reasoning:,}")
|
|
600
|
+
|
|
601
|
+
token_panel = Panel(
|
|
602
|
+
token_table,
|
|
603
|
+
title="[bold]Token Usage[/]",
|
|
604
|
+
title_align="left",
|
|
605
|
+
border_style="green",
|
|
606
|
+
expand=True,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
# Display panels in 1:1:1 ratio layout (Tool Usage | Paths | Token Usage)
|
|
610
|
+
panels = [p for p in [tool_panel, paths_panel, token_panel] if p is not None]
|
|
611
|
+
if panels:
|
|
612
|
+
layout_table = Table.grid(expand=True)
|
|
613
|
+
for _ in panels:
|
|
614
|
+
layout_table.add_column(ratio=1)
|
|
615
|
+
layout_table.add_row(*panels)
|
|
616
|
+
console.print(layout_table)
|
|
617
|
+
console.print()
|
|
618
|
+
|
|
619
|
+
console.rule(style="dim")
|
|
620
|
+
|
|
621
|
+
# Display max turns exceeded error panel (only for top-level agent, and only if no other error)
|
|
622
|
+
if self.finish_params is None and self.max_turns is not None and error is None:
|
|
623
|
+
content = Text()
|
|
624
|
+
content.append("Maximum turns reached\n\n", style="bold")
|
|
625
|
+
content.append("Turns used: ", style="dim")
|
|
626
|
+
content.append(f"{self.max_turns}", style="bold red")
|
|
627
|
+
content.append("\n\n")
|
|
628
|
+
content.append(
|
|
629
|
+
"The agent was not able to finish the task. Consider increasing the max_turns parameter.",
|
|
630
|
+
style="italic",
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
panel = Panel(
|
|
634
|
+
content,
|
|
635
|
+
title="[bold red]⚠ Max Turns Exceeded[/]",
|
|
636
|
+
title_align="left",
|
|
637
|
+
border_style="red",
|
|
638
|
+
padding=(0, 1),
|
|
639
|
+
)
|
|
640
|
+
console.print(panel)
|
|
641
|
+
console.print()
|
|
642
|
+
|
|
643
|
+
def on_step(
|
|
644
|
+
self,
|
|
645
|
+
step: int,
|
|
646
|
+
tool_calls: int = 0,
|
|
647
|
+
input_tokens: int = 0,
|
|
648
|
+
output_tokens: int = 0,
|
|
649
|
+
) -> None:
|
|
650
|
+
"""Report step progress and stats during agent execution."""
|
|
651
|
+
self._current_step = step
|
|
652
|
+
self._tool_calls = tool_calls
|
|
653
|
+
self._input_tokens = input_tokens
|
|
654
|
+
self._output_tokens = output_tokens
|
|
655
|
+
if self._live:
|
|
656
|
+
self._live.update(self._make_spinner())
|
|
657
|
+
|
|
658
|
+
def set_level(self, level: int) -> None:
|
|
659
|
+
"""Set the logging level."""
|
|
660
|
+
self._level = level
|
|
661
|
+
# Also update root logger level
|
|
662
|
+
logging.getLogger().setLevel(level)
|
|
663
|
+
|
|
664
|
+
def is_enabled_for(self, level: int) -> bool:
|
|
665
|
+
"""Check if a given log level is enabled."""
|
|
666
|
+
return level >= self._level
|
|
667
|
+
|
|
668
|
+
# -------------------------------------------------------------------------
|
|
669
|
+
# Standard logging methods (debug, info, warning, error, critical)
|
|
670
|
+
# -------------------------------------------------------------------------
|
|
671
|
+
|
|
672
|
+
def debug(self, message: str, *args: object) -> None:
|
|
673
|
+
"""Log a debug message (dim style)."""
|
|
674
|
+
if self._level <= logging.DEBUG:
|
|
675
|
+
formatted = message % args if args else message
|
|
676
|
+
console.print(f"[dim]{formatted}[/]")
|
|
677
|
+
|
|
678
|
+
def info(self, message: str, *args: object) -> None:
|
|
679
|
+
"""Log an info message."""
|
|
680
|
+
if self._level <= logging.INFO:
|
|
681
|
+
formatted = message % args if args else message
|
|
682
|
+
console.print(formatted)
|
|
683
|
+
|
|
684
|
+
def warning(self, message: str, *args: object) -> None:
|
|
685
|
+
"""Log a warning message (yellow style)."""
|
|
686
|
+
if self._level <= logging.WARNING:
|
|
687
|
+
formatted = message % args if args else message
|
|
688
|
+
console.print(f"[yellow]⚠ {formatted}[/]")
|
|
689
|
+
|
|
690
|
+
def error(self, message: str, *args: object) -> None:
|
|
691
|
+
"""Log an error message (red style)."""
|
|
692
|
+
if self._level <= logging.ERROR:
|
|
693
|
+
formatted = message % args if args else message
|
|
694
|
+
console.print(f"[red]✗ {formatted}[/]")
|
|
695
|
+
|
|
696
|
+
def critical(self, message: str, *args: object) -> None:
|
|
697
|
+
"""Log a critical message (bold red style)."""
|
|
698
|
+
if self._level <= logging.CRITICAL:
|
|
699
|
+
formatted = message % args if args else message
|
|
700
|
+
console.print(f"[bold red]✗ CRITICAL: {formatted}[/]")
|
|
701
|
+
|
|
702
|
+
def exception(self, message: str, *args: object) -> None:
|
|
703
|
+
"""Log an error message with exception traceback (red style with traceback)."""
|
|
704
|
+
if self._level <= logging.ERROR:
|
|
705
|
+
formatted = message % args if args else message
|
|
706
|
+
console.print(f"[red]✗ {formatted}[/]")
|
|
707
|
+
console.print_exception()
|
|
708
|
+
|
|
709
|
+
# -------------------------------------------------------------------------
|
|
710
|
+
# Message Logging Methods (AgentLoggerBase implementation)
|
|
711
|
+
# -------------------------------------------------------------------------
|
|
712
|
+
|
|
713
|
+
def assistant_message(
|
|
714
|
+
self,
|
|
715
|
+
turn: int,
|
|
716
|
+
max_turns: int,
|
|
717
|
+
assistant_message: AssistantMessage,
|
|
718
|
+
) -> None:
|
|
719
|
+
"""Log an assistant message with content and tool calls in a panel.
|
|
720
|
+
|
|
721
|
+
Args:
|
|
722
|
+
turn: Current turn number (1-indexed)
|
|
723
|
+
max_turns: Maximum number of turns
|
|
724
|
+
assistant_message: The assistant's response message
|
|
725
|
+
"""
|
|
726
|
+
if self._level > logging.INFO:
|
|
727
|
+
return
|
|
728
|
+
|
|
729
|
+
# Build panel content
|
|
730
|
+
content = Text()
|
|
731
|
+
|
|
732
|
+
# Add assistant content if present
|
|
733
|
+
if assistant_message.content:
|
|
734
|
+
text = assistant_message.content
|
|
735
|
+
if isinstance(text, list):
|
|
736
|
+
text = "\n".join(str(block) for block in text)
|
|
737
|
+
# Truncate long content
|
|
738
|
+
if len(text) > 500:
|
|
739
|
+
text = text[:500] + "..."
|
|
740
|
+
content.append(text, style="white")
|
|
741
|
+
|
|
742
|
+
# Add tool calls if present
|
|
743
|
+
if assistant_message.tool_calls:
|
|
744
|
+
if assistant_message.content:
|
|
745
|
+
content.append("\n\n")
|
|
746
|
+
content.append("Tool Calls:\n", style="bold magenta")
|
|
747
|
+
for tc in assistant_message.tool_calls:
|
|
748
|
+
args_parsed = json.loads(tc.arguments)
|
|
749
|
+
args_formatted = json.dumps(args_parsed, indent=2, ensure_ascii=False)
|
|
750
|
+
args_preview = args_formatted[:1000] + "..." if len(args_formatted) > 1000 else args_formatted
|
|
751
|
+
content.append(f" 🔧 {tc.name}", style="magenta")
|
|
752
|
+
content.append(args_preview, style="dim")
|
|
753
|
+
|
|
754
|
+
# Create and print panel with agent name in title
|
|
755
|
+
title = f"[bold]AssistantMessage[/bold] │ {self.name} │ Turn {turn}/{max_turns}"
|
|
756
|
+
panel = Panel(content, title=title, title_align="left", border_style="yellow", padding=(0, 1))
|
|
757
|
+
|
|
758
|
+
if self.depth > 0:
|
|
759
|
+
self._print_indented(panel, self.depth * SUBAGENT_INDENT_SPACES)
|
|
760
|
+
else:
|
|
761
|
+
console.print(panel)
|
|
762
|
+
|
|
763
|
+
def user_message(self, user_message: UserMessage) -> None:
|
|
764
|
+
"""Log a user message in a panel.
|
|
765
|
+
|
|
766
|
+
Args:
|
|
767
|
+
user_message: The user's message
|
|
768
|
+
"""
|
|
769
|
+
if self._level > logging.INFO:
|
|
770
|
+
return
|
|
771
|
+
|
|
772
|
+
# Build panel content
|
|
773
|
+
content = Text()
|
|
774
|
+
|
|
775
|
+
# Add user content
|
|
776
|
+
if user_message.content:
|
|
777
|
+
text = user_message.content
|
|
778
|
+
if isinstance(text, list):
|
|
779
|
+
text = "\n".join(str(block) for block in text)
|
|
780
|
+
# Truncate long content
|
|
781
|
+
if len(text) > 500:
|
|
782
|
+
text = text[:500] + "..."
|
|
783
|
+
content.append(text, style="white")
|
|
784
|
+
|
|
785
|
+
# Create and print panel with agent name in title
|
|
786
|
+
title = f"[bold]UserMessage[/bold] │ {self.name}"
|
|
787
|
+
panel = Panel(content, title=title, title_align="left", border_style="blue", padding=(0, 1))
|
|
788
|
+
|
|
789
|
+
if self.depth > 0:
|
|
790
|
+
self._print_indented(panel, self.depth * SUBAGENT_INDENT_SPACES)
|
|
791
|
+
else:
|
|
792
|
+
console.print(panel)
|
|
793
|
+
|
|
794
|
+
def task_message(self, task: str | list[Any]) -> None:
|
|
795
|
+
"""Log the initial task/prompt at the start of a run."""
|
|
796
|
+
if self._level > logging.INFO:
|
|
797
|
+
return
|
|
798
|
+
|
|
799
|
+
# Convert list content to string
|
|
800
|
+
if isinstance(task, list):
|
|
801
|
+
task = "\n".join(str(block) for block in task)
|
|
802
|
+
|
|
803
|
+
# Clean up whitespace from multi-line strings
|
|
804
|
+
# Normalize each line by stripping leading/trailing whitespace and rejoining
|
|
805
|
+
lines = [line.strip() for line in task.split("\n")]
|
|
806
|
+
task = " ".join(line for line in lines if line)
|
|
807
|
+
|
|
808
|
+
# Use "Sub Agent" prefix for nested agents
|
|
809
|
+
prefix = "Sub Agent" if self.depth > 0 else "Agent"
|
|
810
|
+
|
|
811
|
+
if self.depth > 0:
|
|
812
|
+
indent = " " * (self.depth * SUBAGENT_INDENT_SPACES)
|
|
813
|
+
console.print(f"{indent}[bold]{prefix} Task:[/bold]")
|
|
814
|
+
console.print()
|
|
815
|
+
for line in task.split("\n"):
|
|
816
|
+
console.print(f"{indent}{line}")
|
|
817
|
+
else:
|
|
818
|
+
console.print(f"[bold]{prefix} Task:[/bold]")
|
|
819
|
+
console.print()
|
|
820
|
+
console.print(task)
|
|
821
|
+
|
|
822
|
+
console.print() # Add gap after task section
|
|
823
|
+
|
|
824
|
+
def warnings_message(self, warnings: list[str]) -> None:
|
|
825
|
+
"""Display warnings at run start as simple text."""
|
|
826
|
+
if self._level > logging.INFO or not warnings:
|
|
827
|
+
return
|
|
828
|
+
|
|
829
|
+
console.print("[bold orange1]Warnings[/bold orange1]")
|
|
830
|
+
console.print()
|
|
831
|
+
for warning in warnings:
|
|
832
|
+
console.print(f"[orange1]⚠ {warning}[/orange1]")
|
|
833
|
+
console.print() # Add gap between warnings
|
|
834
|
+
|
|
835
|
+
def tool_result(self, tool_message: ToolMessage) -> None:
|
|
836
|
+
"""Log a single tool execution result in a panel with XML syntax highlighting.
|
|
837
|
+
|
|
838
|
+
Args:
|
|
839
|
+
tool_message: The tool execution result
|
|
840
|
+
"""
|
|
841
|
+
if self._level > logging.INFO:
|
|
842
|
+
return
|
|
843
|
+
|
|
844
|
+
tool_name = tool_message.name or "unknown"
|
|
845
|
+
|
|
846
|
+
# Get result content
|
|
847
|
+
result_text = tool_message.content
|
|
848
|
+
if isinstance(result_text, list):
|
|
849
|
+
result_text = "\n".join(str(block) for block in result_text)
|
|
850
|
+
|
|
851
|
+
# Unescape HTML entities (e.g., < -> <, > -> >, & -> &)
|
|
852
|
+
result_text = html.unescape(result_text)
|
|
853
|
+
|
|
854
|
+
# Truncate long results
|
|
855
|
+
if len(result_text) > 1000:
|
|
856
|
+
result_text = result_text[:1000] + "..."
|
|
857
|
+
|
|
858
|
+
# Format as XML with syntax highlighting
|
|
859
|
+
content = Syntax(result_text, "xml", theme="monokai", word_wrap=True)
|
|
860
|
+
|
|
861
|
+
# Status indicator in title with agent name
|
|
862
|
+
status = "✓" if tool_message.args_was_valid else "✗"
|
|
863
|
+
status_style = "green" if tool_message.args_was_valid else "red"
|
|
864
|
+
title = f"[{status_style}]{status}[/{status_style}] [bold]ToolResult[/bold] │ {self.name} │ [green]{tool_name}[/green]"
|
|
865
|
+
|
|
866
|
+
panel = Panel(content, title=title, title_align="left", border_style="green", padding=(0, 1))
|
|
867
|
+
|
|
868
|
+
if self.depth > 0:
|
|
869
|
+
self._print_indented(panel, self.depth * SUBAGENT_INDENT_SPACES)
|
|
870
|
+
else:
|
|
871
|
+
console.print(panel)
|
|
872
|
+
|
|
873
|
+
# -------------------------------------------------------------------------
|
|
874
|
+
# Context Summarization Methods (AgentLoggerBase implementation)
|
|
875
|
+
# -------------------------------------------------------------------------
|
|
876
|
+
|
|
877
|
+
def context_summarization_start(self, pct_used: float, cutoff: float) -> None:
|
|
878
|
+
"""Log context window summarization starting in an orange panel.
|
|
879
|
+
|
|
880
|
+
Args:
|
|
881
|
+
pct_used: Percentage of context window currently used (0.0-1.0)
|
|
882
|
+
cutoff: The threshold that triggered summarization (0.0-1.0)
|
|
883
|
+
"""
|
|
884
|
+
# Build panel content
|
|
885
|
+
content = Text()
|
|
886
|
+
content.append("Context window limit reached\n\n", style="bold")
|
|
887
|
+
content.append("Used: ", style="dim")
|
|
888
|
+
content.append(f"{pct_used:.1%}", style="bold orange1")
|
|
889
|
+
content.append(" │ ", style="dim")
|
|
890
|
+
content.append("Threshold: ", style="dim")
|
|
891
|
+
content.append(f"{cutoff:.1%}", style="bold")
|
|
892
|
+
content.append("\n\n", style="dim")
|
|
893
|
+
content.append("Summarizing conversation history...", style="italic")
|
|
894
|
+
|
|
895
|
+
panel = Panel(
|
|
896
|
+
content,
|
|
897
|
+
title="[bold orange1]📝 Context Summarization[/]",
|
|
898
|
+
title_align="left",
|
|
899
|
+
border_style="orange1",
|
|
900
|
+
padding=(0, 1),
|
|
901
|
+
)
|
|
902
|
+
|
|
903
|
+
if self.depth > 0:
|
|
904
|
+
self._print_indented(panel, self.depth * SUBAGENT_INDENT_SPACES)
|
|
905
|
+
else:
|
|
906
|
+
console.print(panel)
|
|
907
|
+
|
|
908
|
+
def context_summarization_complete(self, summary: str, bridge: str) -> None:
|
|
909
|
+
"""Log the completed context summarization with summary content.
|
|
910
|
+
|
|
911
|
+
Args:
|
|
912
|
+
summary: The generated summary of the conversation
|
|
913
|
+
bridge: The bridge message that will be used to continue the conversation
|
|
914
|
+
"""
|
|
915
|
+
# Truncate long summaries for display
|
|
916
|
+
summary_display = summary
|
|
917
|
+
if len(summary_display) > 800:
|
|
918
|
+
summary_display = summary_display[:800] + "..."
|
|
919
|
+
|
|
920
|
+
# Build panel content
|
|
921
|
+
content = Text()
|
|
922
|
+
content.append("Summary:\n", style="bold")
|
|
923
|
+
content.append(summary_display, style="white")
|
|
924
|
+
|
|
925
|
+
if self._level > logging.INFO:
|
|
926
|
+
bridge_display = bridge
|
|
927
|
+
if len(bridge_display) > 200:
|
|
928
|
+
bridge_display = bridge_display[:200] + "..."
|
|
929
|
+
content.append("\n\n")
|
|
930
|
+
content.append("Bridge Message:\n", style="bold dim")
|
|
931
|
+
content.append(bridge_display, style="dim italic")
|
|
932
|
+
|
|
933
|
+
panel = Panel(
|
|
934
|
+
content,
|
|
935
|
+
title="[bold green]✓ Summary Generated[/]",
|
|
936
|
+
title_align="left",
|
|
937
|
+
border_style="green",
|
|
938
|
+
padding=(0, 1),
|
|
939
|
+
)
|
|
940
|
+
|
|
941
|
+
if self.depth > 0:
|
|
942
|
+
self._print_indented(panel, self.depth * SUBAGENT_INDENT_SPACES)
|
|
943
|
+
else:
|
|
944
|
+
console.print(panel)
|