glaip-sdk 0.0.18__py3-none-any.whl → 0.0.20__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.
- glaip_sdk/_version.py +2 -2
- glaip_sdk/branding.py +27 -2
- glaip_sdk/cli/auth.py +93 -28
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/agents.py +108 -21
- glaip_sdk/cli/commands/configure.py +141 -90
- glaip_sdk/cli/commands/mcps.py +371 -48
- glaip_sdk/cli/commands/models.py +4 -3
- glaip_sdk/cli/commands/tools.py +27 -14
- glaip_sdk/cli/commands/update.py +66 -0
- glaip_sdk/cli/config.py +13 -2
- glaip_sdk/cli/display.py +35 -26
- glaip_sdk/cli/io.py +14 -5
- glaip_sdk/cli/main.py +185 -73
- glaip_sdk/cli/pager.py +2 -1
- glaip_sdk/cli/parsers/json_input.py +62 -14
- glaip_sdk/cli/resolution.py +4 -1
- glaip_sdk/cli/slash/__init__.py +3 -4
- glaip_sdk/cli/slash/agent_session.py +88 -36
- glaip_sdk/cli/slash/prompt.py +20 -48
- glaip_sdk/cli/slash/session.py +440 -189
- glaip_sdk/cli/transcript/__init__.py +71 -0
- glaip_sdk/cli/transcript/cache.py +338 -0
- glaip_sdk/cli/transcript/capture.py +278 -0
- glaip_sdk/cli/transcript/export.py +38 -0
- glaip_sdk/cli/transcript/launcher.py +79 -0
- glaip_sdk/cli/transcript/viewer.py +624 -0
- glaip_sdk/cli/update_notifier.py +29 -5
- glaip_sdk/cli/utils.py +256 -74
- glaip_sdk/client/agents.py +3 -1
- glaip_sdk/client/run_rendering.py +2 -2
- glaip_sdk/icons.py +19 -0
- glaip_sdk/models.py +6 -0
- glaip_sdk/rich_components.py +29 -1
- glaip_sdk/utils/__init__.py +1 -1
- glaip_sdk/utils/client_utils.py +6 -4
- glaip_sdk/utils/display.py +61 -32
- glaip_sdk/utils/rendering/formatting.py +6 -5
- glaip_sdk/utils/rendering/renderer/base.py +213 -66
- glaip_sdk/utils/rendering/renderer/debug.py +73 -16
- glaip_sdk/utils/rendering/renderer/panels.py +27 -15
- glaip_sdk/utils/rendering/renderer/progress.py +61 -38
- glaip_sdk/utils/serialization.py +5 -2
- glaip_sdk/utils/validation.py +1 -2
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/METADATA +1 -1
- glaip_sdk-0.0.20.dist-info/RECORD +80 -0
- glaip_sdk/utils/rich_utils.py +0 -29
- glaip_sdk-0.0.18.dist-info/RECORD +0 -73
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.18.dist-info → glaip_sdk-0.0.20.dist-info}/entry_points.txt +0 -0
glaip_sdk/utils/display.py
CHANGED
|
@@ -4,9 +4,51 @@ Authors:
|
|
|
4
4
|
Raymond Christopher (raymond.christopher@gdplabs.id)
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from typing import Any
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
8
|
|
|
9
|
-
from glaip_sdk.
|
|
9
|
+
from glaip_sdk.branding import SUCCESS, SUCCESS_STYLE
|
|
10
|
+
from glaip_sdk.icons import ICON_AGENT
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING: # pragma: no cover - import-time typing helpers
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
|
|
16
|
+
from glaip_sdk.rich_components import AIPPanel
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _check_rich_available() -> bool:
|
|
20
|
+
"""Check if Rich and our custom components can be imported."""
|
|
21
|
+
try:
|
|
22
|
+
__import__("rich.console")
|
|
23
|
+
__import__("rich.text")
|
|
24
|
+
__import__("glaip_sdk.rich_components")
|
|
25
|
+
return True
|
|
26
|
+
except Exception:
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
RICH_AVAILABLE = _check_rich_available()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _create_console() -> "Console":
|
|
34
|
+
"""Return a Console instance with lazy import to ease mocking."""
|
|
35
|
+
from rich.console import Console # Local import for test friendliness
|
|
36
|
+
|
|
37
|
+
return Console()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _create_text(*args: Any, **kwargs: Any) -> "Text":
|
|
41
|
+
"""Return a Text instance with lazy import to ease mocking."""
|
|
42
|
+
from rich.text import Text # Local import for test friendliness
|
|
43
|
+
|
|
44
|
+
return Text(*args, **kwargs)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _create_panel(*args: Any, **kwargs: Any) -> "AIPPanel":
|
|
48
|
+
"""Return an AIPPanel instance with lazy import to ease mocking."""
|
|
49
|
+
from glaip_sdk.rich_components import AIPPanel # Local import for test friendliness
|
|
50
|
+
|
|
51
|
+
return AIPPanel(*args, **kwargs)
|
|
10
52
|
|
|
11
53
|
|
|
12
54
|
def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
@@ -17,17 +59,11 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
|
17
59
|
title: Title for the output panel
|
|
18
60
|
"""
|
|
19
61
|
if RICH_AVAILABLE:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
25
|
-
|
|
26
|
-
console = Console()
|
|
27
|
-
panel = AIPPanel(
|
|
28
|
-
Text(output, style="green"),
|
|
62
|
+
console = _create_console()
|
|
63
|
+
panel = _create_panel(
|
|
64
|
+
_create_text(output, style=SUCCESS),
|
|
29
65
|
title=title,
|
|
30
|
-
border_style=
|
|
66
|
+
border_style=SUCCESS,
|
|
31
67
|
)
|
|
32
68
|
console.print(panel)
|
|
33
69
|
else:
|
|
@@ -36,7 +72,7 @@ def print_agent_output(output: str, title: str = "Agent Output") -> None:
|
|
|
36
72
|
print("=" * (len(title) + 8))
|
|
37
73
|
|
|
38
74
|
|
|
39
|
-
def print_agent_created(agent: Any, title: str = "
|
|
75
|
+
def print_agent_created(agent: Any, title: str = f"{ICON_AGENT} Agent Created") -> None:
|
|
40
76
|
"""Print agent creation success with rich formatting.
|
|
41
77
|
|
|
42
78
|
Args:
|
|
@@ -44,21 +80,16 @@ def print_agent_created(agent: Any, title: str = "🤖 Agent Created") -> None:
|
|
|
44
80
|
title: Title for the output panel
|
|
45
81
|
"""
|
|
46
82
|
if RICH_AVAILABLE:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
from glaip_sdk.rich_components import AIPPanel
|
|
51
|
-
|
|
52
|
-
console = Console()
|
|
53
|
-
panel = AIPPanel(
|
|
54
|
-
f"[green]✅ Agent '{agent.name}' created successfully![/green]\n\n"
|
|
83
|
+
console = _create_console()
|
|
84
|
+
panel = _create_panel(
|
|
85
|
+
f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' created successfully![/]\n\n"
|
|
55
86
|
f"ID: {agent.id}\n"
|
|
56
87
|
f"Model: {getattr(agent, 'model', 'N/A')}\n"
|
|
57
88
|
f"Type: {getattr(agent, 'type', 'config')}\n"
|
|
58
89
|
f"Framework: {getattr(agent, 'framework', 'langchain')}\n"
|
|
59
90
|
f"Version: {getattr(agent, 'version', '1.0')}",
|
|
60
91
|
title=title,
|
|
61
|
-
border_style=
|
|
92
|
+
border_style=SUCCESS,
|
|
62
93
|
)
|
|
63
94
|
console.print(panel)
|
|
64
95
|
else:
|
|
@@ -77,11 +108,10 @@ def print_agent_updated(agent: Any) -> None:
|
|
|
77
108
|
agent: The updated agent object
|
|
78
109
|
"""
|
|
79
110
|
if RICH_AVAILABLE:
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
console.print(f"[green]✅ Agent '{agent.name}' updated successfully[/green]")
|
|
111
|
+
console = _create_console()
|
|
112
|
+
console.print(
|
|
113
|
+
f"[{SUCCESS_STYLE}]✅ Agent '{agent.name}' updated successfully[/]"
|
|
114
|
+
)
|
|
85
115
|
else:
|
|
86
116
|
print(f"✅ Agent '{agent.name}' updated successfully")
|
|
87
117
|
|
|
@@ -93,10 +123,9 @@ def print_agent_deleted(agent_id: str) -> None:
|
|
|
93
123
|
agent_id: The deleted agent's ID
|
|
94
124
|
"""
|
|
95
125
|
if RICH_AVAILABLE:
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
console.print(f"[green]✅ Agent deleted successfully (ID: {agent_id})[/green]")
|
|
126
|
+
console = _create_console()
|
|
127
|
+
console.print(
|
|
128
|
+
f"[{SUCCESS_STYLE}]✅ Agent deleted successfully (ID: {agent_id})[/]"
|
|
129
|
+
)
|
|
101
130
|
else:
|
|
102
131
|
print(f"✅ Agent deleted successfully (ID: {agent_id})")
|
|
@@ -6,11 +6,14 @@ Authors:
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
9
10
|
import re
|
|
10
11
|
import time
|
|
11
12
|
from collections.abc import Callable
|
|
12
13
|
from typing import Any
|
|
13
14
|
|
|
15
|
+
from glaip_sdk.icons import ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
|
|
16
|
+
|
|
14
17
|
# Constants for argument formatting
|
|
15
18
|
DEFAULT_ARGS_MAX_LEN = 100
|
|
16
19
|
IMPORTANT_PARAMETER_KEYS = [
|
|
@@ -127,8 +130,6 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
|
127
130
|
|
|
128
131
|
# Convert to JSON string and truncate if needed
|
|
129
132
|
try:
|
|
130
|
-
import json
|
|
131
|
-
|
|
132
133
|
args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
|
|
133
134
|
return _truncate_string(args_str, max_len)
|
|
134
135
|
except (TypeError, ValueError, Exception):
|
|
@@ -174,11 +175,11 @@ def get_spinner_char() -> str:
|
|
|
174
175
|
def get_step_icon(step_kind: str) -> str:
|
|
175
176
|
"""Get the appropriate icon for a step kind."""
|
|
176
177
|
if step_kind == "tool":
|
|
177
|
-
return
|
|
178
|
+
return ICON_TOOL_STEP
|
|
178
179
|
if step_kind == "delegate":
|
|
179
|
-
return
|
|
180
|
+
return ICON_DELEGATE
|
|
180
181
|
if step_kind == "agent":
|
|
181
|
-
return
|
|
182
|
+
return ICON_AGENT_STEP
|
|
182
183
|
return ""
|
|
183
184
|
|
|
184
185
|
|
|
@@ -8,16 +8,20 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
-
from dataclasses import dataclass
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from datetime import datetime, timezone
|
|
12
13
|
from time import monotonic
|
|
13
14
|
from typing import Any
|
|
14
15
|
|
|
16
|
+
from rich.align import Align
|
|
15
17
|
from rich.console import Console as RichConsole
|
|
16
18
|
from rich.console import Group
|
|
17
19
|
from rich.live import Live
|
|
18
20
|
from rich.markdown import Markdown
|
|
21
|
+
from rich.spinner import Spinner
|
|
19
22
|
from rich.text import Text
|
|
20
23
|
|
|
24
|
+
from glaip_sdk.icons import ICON_AGENT, ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
|
|
21
25
|
from glaip_sdk.rich_components import AIPPanel
|
|
22
26
|
from glaip_sdk.utils.rendering.formatting import (
|
|
23
27
|
format_main_title,
|
|
@@ -49,6 +53,25 @@ logger = logging.getLogger("glaip_sdk.run_renderer")
|
|
|
49
53
|
LESS_THAN_1MS = "[<1ms]"
|
|
50
54
|
|
|
51
55
|
|
|
56
|
+
def _coerce_received_at(value: Any) -> datetime | None:
|
|
57
|
+
"""Coerce a received_at value to an aware datetime if possible."""
|
|
58
|
+
if value is None:
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
if isinstance(value, datetime):
|
|
62
|
+
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
63
|
+
|
|
64
|
+
if isinstance(value, str):
|
|
65
|
+
try:
|
|
66
|
+
normalised = value.replace("Z", "+00:00")
|
|
67
|
+
dt = datetime.fromisoformat(normalised)
|
|
68
|
+
except ValueError:
|
|
69
|
+
return None
|
|
70
|
+
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
71
|
+
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
|
|
52
75
|
@dataclass
|
|
53
76
|
class RendererState:
|
|
54
77
|
"""Internal state for the renderer."""
|
|
@@ -56,10 +79,13 @@ class RendererState:
|
|
|
56
79
|
buffer: list[str] | None = None
|
|
57
80
|
final_text: str = ""
|
|
58
81
|
streaming_started_at: float | None = None
|
|
59
|
-
|
|
82
|
+
printed_final_output: bool = False
|
|
60
83
|
finalizing_ui: bool = False
|
|
61
84
|
final_duration_seconds: float | None = None
|
|
62
85
|
final_duration_text: str | None = None
|
|
86
|
+
events: list[dict[str, Any]] = field(default_factory=list)
|
|
87
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
88
|
+
streaming_started_event_ts: datetime | None = None
|
|
63
89
|
|
|
64
90
|
def __post_init__(self) -> None:
|
|
65
91
|
"""Initialize renderer state after dataclass creation.
|
|
@@ -127,7 +153,10 @@ class RichStreamRenderer:
|
|
|
127
153
|
|
|
128
154
|
# Set up initial state
|
|
129
155
|
self._started_at = monotonic()
|
|
130
|
-
|
|
156
|
+
try:
|
|
157
|
+
self.state.meta = json.loads(json.dumps(meta))
|
|
158
|
+
except Exception:
|
|
159
|
+
self.state.meta = dict(meta)
|
|
131
160
|
|
|
132
161
|
# Print compact header and user request (parity with old renderer)
|
|
133
162
|
self._render_header(meta)
|
|
@@ -147,7 +176,7 @@ class RichStreamRenderer:
|
|
|
147
176
|
|
|
148
177
|
def _build_header_parts(self, meta: dict[str, Any]) -> list[str]:
|
|
149
178
|
"""Build header text parts from metadata."""
|
|
150
|
-
parts: list[str] = [
|
|
179
|
+
parts: list[str] = [ICON_AGENT]
|
|
151
180
|
agent_name = meta.get("agent_name", "agent")
|
|
152
181
|
if agent_name:
|
|
153
182
|
parts.append(agent_name)
|
|
@@ -193,29 +222,71 @@ class RichStreamRenderer:
|
|
|
193
222
|
)
|
|
194
223
|
)
|
|
195
224
|
|
|
225
|
+
def _ensure_streaming_started_baseline(self, timestamp: float) -> None:
|
|
226
|
+
"""Synchronize streaming start state across renderer components."""
|
|
227
|
+
self.state.streaming_started_at = timestamp
|
|
228
|
+
self.stream_processor.streaming_started_at = timestamp
|
|
229
|
+
self._started_at = timestamp
|
|
230
|
+
|
|
196
231
|
def on_event(self, ev: dict[str, Any]) -> None:
|
|
197
232
|
"""Handle streaming events from the backend."""
|
|
198
|
-
|
|
233
|
+
received_at = self._resolve_received_timestamp(ev)
|
|
234
|
+
self._capture_event(ev, received_at)
|
|
199
235
|
self.stream_processor.reset_event_tracking()
|
|
200
236
|
|
|
201
|
-
|
|
202
|
-
if self.state.streaming_started_at is None:
|
|
203
|
-
self.state.streaming_started_at = monotonic()
|
|
237
|
+
self._sync_stream_start(ev, received_at)
|
|
204
238
|
|
|
205
|
-
# Extract event metadata
|
|
206
239
|
metadata = self.stream_processor.extract_event_metadata(ev)
|
|
207
|
-
|
|
208
|
-
context_id = metadata["context_id"]
|
|
209
|
-
content = metadata["content"]
|
|
240
|
+
self.stream_processor.update_timing(metadata["context_id"])
|
|
210
241
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
242
|
+
self._maybe_render_debug(ev, received_at)
|
|
243
|
+
self._dispatch_event(ev, metadata)
|
|
244
|
+
|
|
245
|
+
def _resolve_received_timestamp(self, ev: dict[str, Any]) -> datetime:
|
|
246
|
+
"""Return the timestamp an event was received, normalising inputs."""
|
|
247
|
+
received_at = _coerce_received_at(ev.get("received_at"))
|
|
248
|
+
if received_at is None:
|
|
249
|
+
received_at = datetime.now(timezone.utc)
|
|
250
|
+
|
|
251
|
+
if self.state.streaming_started_event_ts is None:
|
|
252
|
+
self.state.streaming_started_event_ts = received_at
|
|
253
|
+
|
|
254
|
+
return received_at
|
|
214
255
|
|
|
215
|
-
|
|
216
|
-
self
|
|
256
|
+
def _sync_stream_start(
|
|
257
|
+
self, ev: dict[str, Any], received_at: datetime | None
|
|
258
|
+
) -> None:
|
|
259
|
+
"""Ensure renderer and stream processor share a streaming baseline."""
|
|
260
|
+
baseline = self.state.streaming_started_at
|
|
261
|
+
if baseline is None:
|
|
262
|
+
baseline = monotonic()
|
|
263
|
+
self._ensure_streaming_started_baseline(baseline)
|
|
264
|
+
elif getattr(self.stream_processor, "streaming_started_at", None) is None:
|
|
265
|
+
self._ensure_streaming_started_baseline(baseline)
|
|
266
|
+
|
|
267
|
+
if ev.get("status") == "streaming_started":
|
|
268
|
+
self.state.streaming_started_event_ts = received_at
|
|
269
|
+
self._ensure_streaming_started_baseline(monotonic())
|
|
270
|
+
|
|
271
|
+
def _maybe_render_debug(
|
|
272
|
+
self, ev: dict[str, Any], received_at: datetime
|
|
273
|
+
) -> None: # pragma: no cover - guard rails for verbose mode
|
|
274
|
+
"""Render debug view when verbose mode is enabled."""
|
|
275
|
+
if not self.verbose:
|
|
276
|
+
return
|
|
277
|
+
|
|
278
|
+
render_debug_event(
|
|
279
|
+
ev,
|
|
280
|
+
self.console,
|
|
281
|
+
received_ts=received_at,
|
|
282
|
+
baseline_ts=self.state.streaming_started_event_ts,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def _dispatch_event(self, ev: dict[str, Any], metadata: dict[str, Any]) -> None:
|
|
286
|
+
"""Route events to the appropriate renderer handlers."""
|
|
287
|
+
kind = metadata["kind"]
|
|
288
|
+
content = metadata["content"]
|
|
217
289
|
|
|
218
|
-
# Handle different event types
|
|
219
290
|
if kind == "status":
|
|
220
291
|
self._handle_status_event(ev)
|
|
221
292
|
elif kind == "content":
|
|
@@ -225,14 +296,13 @@ class RichStreamRenderer:
|
|
|
225
296
|
elif kind in {"agent_step", "agent_thinking_step"}:
|
|
226
297
|
self._handle_agent_step_event(ev)
|
|
227
298
|
else:
|
|
228
|
-
# Update live display for unhandled events
|
|
229
299
|
self._ensure_live()
|
|
230
300
|
|
|
231
301
|
def _handle_status_event(self, ev: dict[str, Any]) -> None:
|
|
232
302
|
"""Handle status events."""
|
|
233
303
|
status = ev.get("status")
|
|
234
304
|
if status == "streaming_started":
|
|
235
|
-
|
|
305
|
+
return
|
|
236
306
|
|
|
237
307
|
def _handle_content_event(self, content: str) -> None:
|
|
238
308
|
"""Handle content streaming events."""
|
|
@@ -252,16 +322,7 @@ class RichStreamRenderer:
|
|
|
252
322
|
self._update_final_duration(meta_payload.get("time"))
|
|
253
323
|
|
|
254
324
|
self._ensure_live()
|
|
255
|
-
|
|
256
|
-
# In verbose mode, show the final result in a panel
|
|
257
|
-
if self.verbose and content.strip():
|
|
258
|
-
final_panel = create_final_panel(
|
|
259
|
-
content,
|
|
260
|
-
title=self._final_panel_title(),
|
|
261
|
-
theme=self.cfg.theme,
|
|
262
|
-
)
|
|
263
|
-
self.console.print(final_panel)
|
|
264
|
-
self.state.printed_final_panel = True
|
|
325
|
+
self._print_final_panel_if_needed()
|
|
265
326
|
|
|
266
327
|
def _handle_agent_step_event(self, ev: dict[str, Any]) -> None:
|
|
267
328
|
"""Handle agent step events."""
|
|
@@ -307,17 +368,22 @@ class RichStreamRenderer:
|
|
|
307
368
|
self._shutdown_live()
|
|
308
369
|
|
|
309
370
|
def _print_final_panel_if_needed(self) -> None:
|
|
310
|
-
"""Print final result
|
|
311
|
-
if self.
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
371
|
+
"""Print final result when configuration requires it."""
|
|
372
|
+
if self.state.printed_final_output:
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
body = (self.state.final_text or "".join(self.state.buffer) or "").strip()
|
|
376
|
+
if not body:
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
if self.verbose:
|
|
380
|
+
final_panel = create_final_panel(
|
|
381
|
+
body,
|
|
382
|
+
title=self._final_panel_title(),
|
|
383
|
+
theme=self.cfg.theme,
|
|
384
|
+
)
|
|
385
|
+
self.console.print(final_panel)
|
|
386
|
+
self.state.printed_final_output = True
|
|
321
387
|
|
|
322
388
|
def on_complete(self, stats: RunStats) -> None:
|
|
323
389
|
"""Handle completion event."""
|
|
@@ -348,7 +414,7 @@ class RichStreamRenderer:
|
|
|
348
414
|
# Stop live display
|
|
349
415
|
self._stop_live_display()
|
|
350
416
|
|
|
351
|
-
#
|
|
417
|
+
# Render final output based on configuration
|
|
352
418
|
self._print_final_panel_if_needed()
|
|
353
419
|
|
|
354
420
|
def _ensure_live(self) -> None:
|
|
@@ -469,6 +535,37 @@ class RichStreamRenderer:
|
|
|
469
535
|
if self.cfg.live:
|
|
470
536
|
self._ensure_live()
|
|
471
537
|
|
|
538
|
+
# ------------------------------------------------------------------
|
|
539
|
+
# Transcript helpers
|
|
540
|
+
# ------------------------------------------------------------------
|
|
541
|
+
def _capture_event(
|
|
542
|
+
self, ev: dict[str, Any], received_at: datetime | None = None
|
|
543
|
+
) -> None:
|
|
544
|
+
"""Capture a deep copy of SSE events for transcript replay."""
|
|
545
|
+
try:
|
|
546
|
+
captured = json.loads(json.dumps(ev))
|
|
547
|
+
except Exception:
|
|
548
|
+
captured = ev
|
|
549
|
+
|
|
550
|
+
if received_at is not None:
|
|
551
|
+
try:
|
|
552
|
+
captured["received_at"] = received_at.isoformat()
|
|
553
|
+
except Exception:
|
|
554
|
+
try:
|
|
555
|
+
captured["received_at"] = str(received_at)
|
|
556
|
+
except Exception:
|
|
557
|
+
captured["received_at"] = repr(received_at)
|
|
558
|
+
|
|
559
|
+
self.state.events.append(captured)
|
|
560
|
+
|
|
561
|
+
def get_aggregated_output(self) -> str:
|
|
562
|
+
"""Return the concatenated assistant output collected so far."""
|
|
563
|
+
return ("".join(self.state.buffer or [])).strip()
|
|
564
|
+
|
|
565
|
+
def get_transcript_events(self) -> list[dict[str, Any]]:
|
|
566
|
+
"""Return captured SSE events."""
|
|
567
|
+
return list(self.state.events)
|
|
568
|
+
|
|
472
569
|
def _maybe_insert_thinking_gap(
|
|
473
570
|
self, task_id: str | None, context_id: str | None
|
|
474
571
|
) -> None:
|
|
@@ -993,11 +1090,11 @@ class RichStreamRenderer:
|
|
|
993
1090
|
def _get_step_icon(self, step_kind: str) -> str:
|
|
994
1091
|
"""Get icon for step kind."""
|
|
995
1092
|
if step_kind == "tool":
|
|
996
|
-
return
|
|
1093
|
+
return ICON_TOOL_STEP
|
|
997
1094
|
elif step_kind == "delegate":
|
|
998
|
-
return
|
|
1095
|
+
return ICON_DELEGATE
|
|
999
1096
|
elif step_kind == "agent":
|
|
1000
|
-
return
|
|
1097
|
+
return ICON_AGENT_STEP
|
|
1001
1098
|
return ""
|
|
1002
1099
|
|
|
1003
1100
|
def _format_step_status(self, step: Step) -> str:
|
|
@@ -1049,30 +1146,64 @@ class RichStreamRenderer:
|
|
|
1049
1146
|
running_by_ctx.setdefault(key, []).append(st)
|
|
1050
1147
|
return running_by_ctx
|
|
1051
1148
|
|
|
1052
|
-
def
|
|
1149
|
+
def _is_parallel_tool(
|
|
1150
|
+
self,
|
|
1151
|
+
step: Step,
|
|
1152
|
+
running_by_ctx: dict[tuple[str | None, str | None], list],
|
|
1153
|
+
) -> bool:
|
|
1154
|
+
"""Return True if multiple tools are running in the same context."""
|
|
1155
|
+
key = (step.task_id, step.context_id)
|
|
1156
|
+
return len(running_by_ctx.get(key, [])) > 1
|
|
1157
|
+
|
|
1158
|
+
def _compose_step_renderable(
|
|
1159
|
+
self,
|
|
1160
|
+
step: Step,
|
|
1161
|
+
running_by_ctx: dict[tuple[str | None, str | None], list],
|
|
1162
|
+
) -> Any:
|
|
1163
|
+
"""Compose a single renderable for the steps panel."""
|
|
1164
|
+
finished = is_step_finished(step)
|
|
1165
|
+
status_br = self._format_step_status(step)
|
|
1166
|
+
display_name = self._get_step_display_name(step)
|
|
1167
|
+
|
|
1168
|
+
if (
|
|
1169
|
+
not finished
|
|
1170
|
+
and step.kind == "tool"
|
|
1171
|
+
and self._is_parallel_tool(step, running_by_ctx)
|
|
1172
|
+
):
|
|
1173
|
+
status_br = status_br.replace("]", " 🔄]")
|
|
1174
|
+
|
|
1175
|
+
icon = self._get_step_icon(step.kind)
|
|
1176
|
+
text_line = Text(style="dim")
|
|
1177
|
+
text_line.append(icon)
|
|
1178
|
+
text_line.append(" ")
|
|
1179
|
+
text_line.append(display_name)
|
|
1180
|
+
if status_br:
|
|
1181
|
+
text_line.append(" ")
|
|
1182
|
+
text_line.append(status_br)
|
|
1183
|
+
if finished:
|
|
1184
|
+
text_line.append(" ✓")
|
|
1185
|
+
|
|
1186
|
+
if finished:
|
|
1187
|
+
return text_line
|
|
1188
|
+
|
|
1189
|
+
spinner = Spinner("dots", text=text_line, style="dim")
|
|
1190
|
+
return Align.left(spinner)
|
|
1191
|
+
|
|
1192
|
+
def _render_steps_text(self) -> Any:
|
|
1053
1193
|
"""Render the steps panel content."""
|
|
1054
1194
|
if not (self.steps.order or self.steps.children):
|
|
1055
1195
|
return Text("No steps yet", style="dim")
|
|
1056
1196
|
|
|
1057
1197
|
running_by_ctx = self._check_parallel_tools()
|
|
1058
|
-
|
|
1059
|
-
|
|
1198
|
+
renderables: list[Any] = []
|
|
1060
1199
|
for sid in self.steps.order:
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
display_name = self._get_step_display_name(st)
|
|
1064
|
-
tail = " ✓" if is_step_finished(st) else ""
|
|
1065
|
-
|
|
1066
|
-
# Add parallel indicator for running tools
|
|
1067
|
-
if st.kind == "tool" and not is_step_finished(st):
|
|
1068
|
-
key = (st.task_id, st.context_id)
|
|
1069
|
-
if len(running_by_ctx.get(key, [])) > 1:
|
|
1070
|
-
status_br = status_br.replace("]", " 🔄]")
|
|
1200
|
+
line = self._compose_step_renderable(self.steps.by_id[sid], running_by_ctx)
|
|
1201
|
+
renderables.append(line)
|
|
1071
1202
|
|
|
1072
|
-
|
|
1073
|
-
|
|
1203
|
+
if not renderables:
|
|
1204
|
+
return Text("No steps yet", style="dim")
|
|
1074
1205
|
|
|
1075
|
-
return
|
|
1206
|
+
return Group(*renderables)
|
|
1076
1207
|
|
|
1077
1208
|
def _should_skip_finished_panel(self, sid: str, status: str) -> bool:
|
|
1078
1209
|
"""Check if a finished panel should be skipped."""
|
|
@@ -1154,18 +1285,27 @@ class RichStreamRenderer:
|
|
|
1154
1285
|
return self._format_elapsed_time(dur) if isinstance(dur, int | float) else None
|
|
1155
1286
|
|
|
1156
1287
|
def _process_running_tool_panel(
|
|
1157
|
-
self,
|
|
1158
|
-
|
|
1288
|
+
self,
|
|
1289
|
+
title: str,
|
|
1290
|
+
meta: dict[str, Any],
|
|
1291
|
+
body: str,
|
|
1292
|
+
*,
|
|
1293
|
+
include_spinner: bool = False,
|
|
1294
|
+
) -> tuple[str, str] | tuple[str, str, str | None]:
|
|
1159
1295
|
"""Process a running tool panel."""
|
|
1160
1296
|
elapsed_str = self._calculate_elapsed_time(meta)
|
|
1161
1297
|
adjusted_title = f"{title} · {elapsed_str}"
|
|
1162
1298
|
chip = f"⏱ {elapsed_str}"
|
|
1299
|
+
spinner_message: str | None = None
|
|
1163
1300
|
|
|
1164
|
-
if not body:
|
|
1165
|
-
body =
|
|
1301
|
+
if not body.strip():
|
|
1302
|
+
body = ""
|
|
1303
|
+
spinner_message = f"{title} running... {elapsed_str}"
|
|
1166
1304
|
else:
|
|
1167
1305
|
body = f"{body}\n\n{chip}"
|
|
1168
1306
|
|
|
1307
|
+
if include_spinner:
|
|
1308
|
+
return adjusted_title, body, spinner_message
|
|
1169
1309
|
return adjusted_title, body
|
|
1170
1310
|
|
|
1171
1311
|
def _process_finished_tool_panel(self, title: str, meta: dict[str, Any]) -> str:
|
|
@@ -1188,8 +1328,12 @@ class RichStreamRenderer:
|
|
|
1188
1328
|
body = "".join(chunks)
|
|
1189
1329
|
adjusted_title = title
|
|
1190
1330
|
|
|
1331
|
+
spinner_message: str | None = None
|
|
1332
|
+
|
|
1191
1333
|
if status == "running":
|
|
1192
|
-
adjusted_title, body = self._process_running_tool_panel(
|
|
1334
|
+
adjusted_title, body, spinner_message = self._process_running_tool_panel(
|
|
1335
|
+
title, meta, body, include_spinner=True
|
|
1336
|
+
)
|
|
1193
1337
|
elif status == "finished":
|
|
1194
1338
|
adjusted_title = self._process_finished_tool_panel(title, meta)
|
|
1195
1339
|
|
|
@@ -1199,10 +1343,13 @@ class RichStreamRenderer:
|
|
|
1199
1343
|
status=status,
|
|
1200
1344
|
theme=self.cfg.theme,
|
|
1201
1345
|
is_delegation=is_delegation,
|
|
1346
|
+
spinner_message=spinner_message,
|
|
1202
1347
|
)
|
|
1203
1348
|
|
|
1204
1349
|
def _render_tool_panels(self) -> list[AIPPanel]:
|
|
1205
1350
|
"""Render tool execution output panels."""
|
|
1351
|
+
if not getattr(self.cfg, "show_delegate_tool_panels", False):
|
|
1352
|
+
return []
|
|
1206
1353
|
panels: list[AIPPanel] = []
|
|
1207
1354
|
for sid in self.tool_order:
|
|
1208
1355
|
meta = self.tool_panels.get(sid) or {}
|