glaip-sdk 0.1.0__py3-none-any.whl → 0.6.10__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/__init__.py +5 -2
- glaip_sdk/_version.py +10 -3
- glaip_sdk/agents/__init__.py +27 -0
- glaip_sdk/agents/base.py +1191 -0
- glaip_sdk/branding.py +15 -6
- glaip_sdk/cli/account_store.py +540 -0
- glaip_sdk/cli/agent_config.py +2 -6
- glaip_sdk/cli/auth.py +265 -45
- glaip_sdk/cli/commands/__init__.py +2 -2
- glaip_sdk/cli/commands/accounts.py +746 -0
- glaip_sdk/cli/commands/agents.py +251 -173
- glaip_sdk/cli/commands/common_config.py +101 -0
- glaip_sdk/cli/commands/configure.py +735 -143
- glaip_sdk/cli/commands/mcps.py +266 -134
- glaip_sdk/cli/commands/models.py +13 -9
- glaip_sdk/cli/commands/tools.py +67 -88
- glaip_sdk/cli/commands/transcripts.py +755 -0
- glaip_sdk/cli/commands/update.py +3 -8
- glaip_sdk/cli/config.py +49 -7
- glaip_sdk/cli/constants.py +38 -0
- glaip_sdk/cli/context.py +8 -0
- glaip_sdk/cli/core/__init__.py +79 -0
- glaip_sdk/cli/core/context.py +124 -0
- glaip_sdk/cli/core/output.py +846 -0
- glaip_sdk/cli/core/prompting.py +649 -0
- glaip_sdk/cli/core/rendering.py +187 -0
- glaip_sdk/cli/display.py +45 -32
- glaip_sdk/cli/hints.py +57 -0
- glaip_sdk/cli/io.py +14 -17
- glaip_sdk/cli/main.py +232 -143
- glaip_sdk/cli/masking.py +21 -33
- glaip_sdk/cli/mcp_validators.py +5 -15
- glaip_sdk/cli/pager.py +12 -19
- glaip_sdk/cli/parsers/__init__.py +1 -3
- glaip_sdk/cli/parsers/json_input.py +11 -22
- glaip_sdk/cli/resolution.py +3 -9
- glaip_sdk/cli/rich_helpers.py +1 -3
- glaip_sdk/cli/slash/__init__.py +0 -9
- glaip_sdk/cli/slash/accounts_controller.py +578 -0
- glaip_sdk/cli/slash/accounts_shared.py +75 -0
- glaip_sdk/cli/slash/agent_session.py +65 -29
- glaip_sdk/cli/slash/prompt.py +24 -10
- glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
- glaip_sdk/cli/slash/session.py +807 -225
- glaip_sdk/cli/slash/tui/__init__.py +9 -0
- glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
- glaip_sdk/cli/slash/tui/accounts_app.py +876 -0
- glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
- glaip_sdk/cli/slash/tui/loading.py +58 -0
- glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
- glaip_sdk/cli/transcript/__init__.py +12 -52
- glaip_sdk/cli/transcript/cache.py +258 -60
- glaip_sdk/cli/transcript/capture.py +72 -21
- glaip_sdk/cli/transcript/history.py +815 -0
- glaip_sdk/cli/transcript/launcher.py +1 -3
- glaip_sdk/cli/transcript/viewer.py +79 -499
- glaip_sdk/cli/update_notifier.py +177 -24
- glaip_sdk/cli/utils.py +242 -1308
- glaip_sdk/cli/validators.py +16 -18
- glaip_sdk/client/__init__.py +2 -1
- glaip_sdk/client/_agent_payloads.py +53 -37
- glaip_sdk/client/agent_runs.py +147 -0
- glaip_sdk/client/agents.py +320 -92
- glaip_sdk/client/base.py +78 -35
- glaip_sdk/client/main.py +19 -10
- glaip_sdk/client/mcps.py +123 -15
- glaip_sdk/client/run_rendering.py +136 -101
- glaip_sdk/client/shared.py +21 -0
- glaip_sdk/client/tools.py +163 -34
- glaip_sdk/client/validators.py +20 -48
- glaip_sdk/config/constants.py +11 -0
- glaip_sdk/exceptions.py +1 -3
- glaip_sdk/mcps/__init__.py +21 -0
- glaip_sdk/mcps/base.py +345 -0
- glaip_sdk/models/__init__.py +90 -0
- glaip_sdk/models/agent.py +47 -0
- glaip_sdk/models/agent_runs.py +116 -0
- glaip_sdk/models/common.py +42 -0
- glaip_sdk/models/mcp.py +33 -0
- glaip_sdk/models/tool.py +33 -0
- glaip_sdk/payload_schemas/__init__.py +1 -13
- glaip_sdk/payload_schemas/agent.py +1 -3
- glaip_sdk/registry/__init__.py +55 -0
- glaip_sdk/registry/agent.py +164 -0
- glaip_sdk/registry/base.py +139 -0
- glaip_sdk/registry/mcp.py +253 -0
- glaip_sdk/registry/tool.py +232 -0
- glaip_sdk/rich_components.py +58 -2
- glaip_sdk/runner/__init__.py +59 -0
- glaip_sdk/runner/base.py +84 -0
- glaip_sdk/runner/deps.py +115 -0
- glaip_sdk/runner/langgraph.py +706 -0
- glaip_sdk/runner/mcp_adapter/__init__.py +13 -0
- glaip_sdk/runner/mcp_adapter/base_mcp_adapter.py +43 -0
- glaip_sdk/runner/mcp_adapter/langchain_mcp_adapter.py +257 -0
- glaip_sdk/runner/mcp_adapter/mcp_config_builder.py +95 -0
- glaip_sdk/runner/tool_adapter/__init__.py +18 -0
- glaip_sdk/runner/tool_adapter/base_tool_adapter.py +44 -0
- glaip_sdk/runner/tool_adapter/langchain_tool_adapter.py +219 -0
- glaip_sdk/tools/__init__.py +22 -0
- glaip_sdk/tools/base.py +435 -0
- glaip_sdk/utils/__init__.py +58 -12
- glaip_sdk/utils/a2a/__init__.py +34 -0
- glaip_sdk/utils/a2a/event_processor.py +188 -0
- glaip_sdk/utils/agent_config.py +4 -14
- glaip_sdk/utils/bundler.py +267 -0
- glaip_sdk/utils/client.py +111 -0
- glaip_sdk/utils/client_utils.py +46 -28
- glaip_sdk/utils/datetime_helpers.py +58 -0
- glaip_sdk/utils/discovery.py +78 -0
- glaip_sdk/utils/display.py +25 -21
- glaip_sdk/utils/export.py +143 -0
- glaip_sdk/utils/general.py +1 -36
- glaip_sdk/utils/import_export.py +15 -16
- glaip_sdk/utils/import_resolver.py +492 -0
- glaip_sdk/utils/instructions.py +101 -0
- glaip_sdk/utils/rendering/__init__.py +115 -1
- glaip_sdk/utils/rendering/formatting.py +7 -35
- glaip_sdk/utils/rendering/layout/__init__.py +64 -0
- glaip_sdk/utils/rendering/{renderer → layout}/panels.py +10 -3
- glaip_sdk/utils/rendering/{renderer → layout}/progress.py +73 -12
- glaip_sdk/utils/rendering/layout/summary.py +74 -0
- glaip_sdk/utils/rendering/layout/transcript.py +606 -0
- glaip_sdk/utils/rendering/models.py +3 -6
- glaip_sdk/utils/rendering/renderer/__init__.py +9 -49
- glaip_sdk/utils/rendering/renderer/base.py +258 -1577
- glaip_sdk/utils/rendering/renderer/config.py +1 -5
- glaip_sdk/utils/rendering/renderer/debug.py +30 -34
- glaip_sdk/utils/rendering/renderer/factory.py +138 -0
- glaip_sdk/utils/rendering/renderer/stream.py +10 -51
- glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
- glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
- glaip_sdk/utils/rendering/renderer/toggle.py +1 -3
- glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
- glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
- glaip_sdk/utils/rendering/state.py +204 -0
- glaip_sdk/utils/rendering/step_tree_state.py +1 -3
- glaip_sdk/utils/rendering/steps/__init__.py +34 -0
- glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +76 -517
- glaip_sdk/utils/rendering/steps/format.py +176 -0
- glaip_sdk/utils/rendering/steps/manager.py +387 -0
- glaip_sdk/utils/rendering/timing.py +36 -0
- glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
- glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
- glaip_sdk/utils/resource_refs.py +29 -26
- glaip_sdk/utils/runtime_config.py +425 -0
- glaip_sdk/utils/serialization.py +32 -46
- glaip_sdk/utils/sync.py +142 -0
- glaip_sdk/utils/tool_detection.py +33 -0
- glaip_sdk/utils/validation.py +20 -28
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/METADATA +42 -4
- glaip_sdk-0.6.10.dist-info/RECORD +159 -0
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/WHEEL +1 -1
- glaip_sdk/models.py +0 -259
- glaip_sdk-0.1.0.dist-info/RECORD +0 -82
- {glaip_sdk-0.1.0.dist-info → glaip_sdk-0.6.10.dist-info}/entry_points.txt +0 -0
|
@@ -8,8 +8,6 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import json
|
|
10
10
|
import logging
|
|
11
|
-
from collections.abc import Iterable
|
|
12
|
-
from dataclasses import dataclass, field
|
|
13
11
|
from datetime import datetime, timezone
|
|
14
12
|
from time import monotonic
|
|
15
13
|
from typing import Any
|
|
@@ -18,154 +16,64 @@ from rich.console import Console as RichConsole
|
|
|
18
16
|
from rich.console import Group
|
|
19
17
|
from rich.live import Live
|
|
20
18
|
from rich.markdown import Markdown
|
|
21
|
-
from rich.measure import Measurement
|
|
22
19
|
from rich.spinner import Spinner
|
|
23
20
|
from rich.text import Text
|
|
24
21
|
|
|
25
22
|
from glaip_sdk.icons import ICON_AGENT, ICON_AGENT_STEP, ICON_DELEGATE, ICON_TOOL_STEP
|
|
26
23
|
from glaip_sdk.rich_components import AIPPanel
|
|
27
24
|
from glaip_sdk.utils.rendering.formatting import (
|
|
28
|
-
build_connector_prefix,
|
|
29
25
|
format_main_title,
|
|
30
|
-
get_spinner_char,
|
|
31
|
-
glyph_for_status,
|
|
32
26
|
is_step_finished,
|
|
33
27
|
normalise_display_label,
|
|
34
|
-
pretty_args,
|
|
35
|
-
redact_sensitive,
|
|
36
28
|
)
|
|
37
29
|
from glaip_sdk.utils.rendering.models import RunStats, Step
|
|
38
|
-
from glaip_sdk.utils.rendering.
|
|
39
|
-
from glaip_sdk.utils.rendering.
|
|
40
|
-
|
|
41
|
-
create_final_panel,
|
|
42
|
-
create_main_panel,
|
|
43
|
-
create_tool_panel,
|
|
44
|
-
)
|
|
45
|
-
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
30
|
+
from glaip_sdk.utils.rendering.layout.panels import create_main_panel
|
|
31
|
+
from glaip_sdk.utils.rendering.layout.progress import (
|
|
32
|
+
build_progress_footer,
|
|
46
33
|
format_elapsed_time,
|
|
47
|
-
format_tool_title,
|
|
48
34
|
format_working_indicator,
|
|
49
|
-
|
|
35
|
+
get_spinner_char,
|
|
50
36
|
is_delegation_tool,
|
|
51
37
|
)
|
|
38
|
+
from glaip_sdk.utils.rendering.layout.summary import render_summary_panels
|
|
39
|
+
from glaip_sdk.utils.rendering.layout.transcript import (
|
|
40
|
+
DEFAULT_TRANSCRIPT_THEME,
|
|
41
|
+
TranscriptSnapshot,
|
|
42
|
+
build_final_panel,
|
|
43
|
+
build_transcript_snapshot,
|
|
44
|
+
build_transcript_view,
|
|
45
|
+
extract_query_from_meta,
|
|
46
|
+
format_final_panel_title,
|
|
47
|
+
)
|
|
48
|
+
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
49
|
+
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
52
50
|
from glaip_sdk.utils.rendering.renderer.stream import StreamProcessor
|
|
53
|
-
from glaip_sdk.utils.rendering.
|
|
51
|
+
from glaip_sdk.utils.rendering.renderer.thinking import ThinkingScopeController
|
|
52
|
+
from glaip_sdk.utils.rendering.renderer.tool_panels import ToolPanelController
|
|
53
|
+
from glaip_sdk.utils.rendering.renderer.transcript_mode import TranscriptModeMixin
|
|
54
|
+
from glaip_sdk.utils.rendering.state import (
|
|
55
|
+
RendererState,
|
|
56
|
+
TranscriptBuffer,
|
|
57
|
+
coerce_received_at,
|
|
58
|
+
truncate_display,
|
|
59
|
+
)
|
|
60
|
+
from glaip_sdk.utils.rendering.steps import (
|
|
61
|
+
StepManager,
|
|
62
|
+
format_step_label,
|
|
63
|
+
)
|
|
64
|
+
from glaip_sdk.utils.rendering.timing import coerce_server_time
|
|
65
|
+
|
|
66
|
+
_NO_STEPS_TEXT = Text("No steps yet", style="dim")
|
|
54
67
|
|
|
55
68
|
# Configure logger
|
|
56
69
|
logger = logging.getLogger("glaip_sdk.run_renderer")
|
|
57
70
|
|
|
58
71
|
# Constants
|
|
59
|
-
LESS_THAN_1MS = "[<1ms]"
|
|
60
|
-
FINISHED_STATUS_HINTS = {
|
|
61
|
-
"finished",
|
|
62
|
-
"success",
|
|
63
|
-
"succeeded",
|
|
64
|
-
"completed",
|
|
65
|
-
"failed",
|
|
66
|
-
"stopped",
|
|
67
|
-
"error",
|
|
68
|
-
}
|
|
69
72
|
RUNNING_STATUS_HINTS = {"running", "started", "pending", "working"}
|
|
70
73
|
ARGS_VALUE_MAX_LEN = 160
|
|
71
|
-
STATUS_ICON_STYLES = {
|
|
72
|
-
"success": "green",
|
|
73
|
-
"failed": "red",
|
|
74
|
-
"warning": "yellow",
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _coerce_received_at(value: Any) -> datetime | None:
|
|
79
|
-
"""Coerce a received_at value to an aware datetime if possible."""
|
|
80
|
-
if value is None:
|
|
81
|
-
return None
|
|
82
|
-
|
|
83
|
-
if isinstance(value, datetime):
|
|
84
|
-
return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
|
|
85
|
-
|
|
86
|
-
if isinstance(value, str):
|
|
87
|
-
try:
|
|
88
|
-
normalised = value.replace("Z", "+00:00")
|
|
89
|
-
dt = datetime.fromisoformat(normalised)
|
|
90
|
-
except ValueError:
|
|
91
|
-
return None
|
|
92
|
-
return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
|
|
93
|
-
|
|
94
|
-
return None
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _truncate_display(text: str | None, limit: int = 160) -> str:
|
|
98
|
-
"""Return text capped at the given character limit with ellipsis."""
|
|
99
|
-
if not text:
|
|
100
|
-
return ""
|
|
101
|
-
stripped = str(text).strip()
|
|
102
|
-
if len(stripped) <= limit:
|
|
103
|
-
return stripped
|
|
104
|
-
return stripped[: limit - 1] + "…"
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@dataclass
|
|
108
|
-
class RendererState:
|
|
109
|
-
"""Internal state for the renderer."""
|
|
110
|
-
|
|
111
|
-
buffer: list[str] | None = None
|
|
112
|
-
final_text: str = ""
|
|
113
|
-
streaming_started_at: float | None = None
|
|
114
|
-
printed_final_output: bool = False
|
|
115
|
-
finalizing_ui: bool = False
|
|
116
|
-
final_duration_seconds: float | None = None
|
|
117
|
-
final_duration_text: str | None = None
|
|
118
|
-
events: list[dict[str, Any]] = field(default_factory=list)
|
|
119
|
-
meta: dict[str, Any] = field(default_factory=dict)
|
|
120
|
-
streaming_started_event_ts: datetime | None = None
|
|
121
|
-
|
|
122
|
-
def __post_init__(self) -> None:
|
|
123
|
-
"""Initialize renderer state after dataclass creation.
|
|
124
|
-
|
|
125
|
-
Ensures buffer is initialized as an empty list if not provided.
|
|
126
|
-
"""
|
|
127
|
-
if self.buffer is None:
|
|
128
|
-
self.buffer = []
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
@dataclass
|
|
132
|
-
class ThinkingScopeState:
|
|
133
|
-
"""Runtime bookkeeping for deterministic thinking spans."""
|
|
134
74
|
|
|
135
|
-
anchor_id: str
|
|
136
|
-
task_id: str | None
|
|
137
|
-
context_id: str | None
|
|
138
|
-
anchor_started_at: float | None = None
|
|
139
|
-
anchor_finished_at: float | None = None
|
|
140
|
-
idle_started_at: float | None = None
|
|
141
|
-
idle_started_monotonic: float | None = None
|
|
142
|
-
active_thinking_id: str | None = None
|
|
143
|
-
running_children: set[str] = field(default_factory=set)
|
|
144
|
-
closed: bool = False
|
|
145
75
|
|
|
146
|
-
|
|
147
|
-
class TrailingSpinnerLine:
|
|
148
|
-
"""Render a text line with a trailing animated Rich spinner."""
|
|
149
|
-
|
|
150
|
-
def __init__(self, base_text: Text, spinner: Spinner) -> None:
|
|
151
|
-
"""Initialize spinner line with base text and spinner component."""
|
|
152
|
-
self._base_text = base_text
|
|
153
|
-
self._spinner = spinner
|
|
154
|
-
|
|
155
|
-
def __rich_console__(self, console: RichConsole, options: Any) -> Any:
|
|
156
|
-
"""Render the text with trailing animated spinner."""
|
|
157
|
-
spinner_render = self._spinner.render(console.get_time())
|
|
158
|
-
combined = Text.assemble(self._base_text.copy(), " ", spinner_render)
|
|
159
|
-
yield combined
|
|
160
|
-
|
|
161
|
-
def __rich_measure__(self, console: RichConsole, options: Any) -> Measurement:
|
|
162
|
-
"""Measure the combined text and spinner dimensions."""
|
|
163
|
-
snapshot = self._spinner.render(0)
|
|
164
|
-
combined = Text.assemble(self._base_text.copy(), " ", snapshot)
|
|
165
|
-
return Measurement.get(console, options, combined)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
class RichStreamRenderer:
|
|
76
|
+
class RichStreamRenderer(TranscriptModeMixin):
|
|
169
77
|
"""Live, modern terminal renderer for agent execution with rich visual output."""
|
|
170
78
|
|
|
171
79
|
def __init__(
|
|
@@ -174,6 +82,8 @@ class RichStreamRenderer:
|
|
|
174
82
|
*,
|
|
175
83
|
cfg: RendererConfig | None = None,
|
|
176
84
|
verbose: bool = False,
|
|
85
|
+
transcript_buffer: TranscriptBuffer | None = None,
|
|
86
|
+
callbacks: dict[str, Any] | None = None,
|
|
177
87
|
) -> None:
|
|
178
88
|
"""Initialize the renderer.
|
|
179
89
|
|
|
@@ -181,7 +91,10 @@ class RichStreamRenderer:
|
|
|
181
91
|
console: Rich console instance
|
|
182
92
|
cfg: Renderer configuration
|
|
183
93
|
verbose: Whether to enable verbose mode
|
|
94
|
+
transcript_buffer: Optional transcript buffer for capturing output
|
|
95
|
+
callbacks: Optional dictionary of callback functions
|
|
184
96
|
"""
|
|
97
|
+
super().__init__()
|
|
185
98
|
self.console = console or RichConsole()
|
|
186
99
|
self.cfg = cfg or RendererConfig()
|
|
187
100
|
self.verbose = verbose
|
|
@@ -189,16 +102,32 @@ class RichStreamRenderer:
|
|
|
189
102
|
# Initialize components
|
|
190
103
|
self.stream_processor = StreamProcessor()
|
|
191
104
|
self.state = RendererState()
|
|
105
|
+
if transcript_buffer is not None:
|
|
106
|
+
self.state.buffer = transcript_buffer
|
|
107
|
+
|
|
108
|
+
self._callbacks = callbacks or {}
|
|
192
109
|
|
|
193
110
|
# Initialize step manager and other state
|
|
194
111
|
self.steps = StepManager(max_steps=self.cfg.summary_max_steps)
|
|
195
112
|
# Live display instance (single source of truth)
|
|
196
113
|
self.live: Live | None = None
|
|
197
114
|
self._step_spinners: dict[str, Spinner] = {}
|
|
115
|
+
self._last_steps_panel_template: Any | None = None
|
|
198
116
|
|
|
199
117
|
# Tool tracking and thinking scopes
|
|
200
|
-
self.
|
|
201
|
-
self.
|
|
118
|
+
self._step_server_start_times: dict[str, float] = {}
|
|
119
|
+
self.tool_controller = ToolPanelController(
|
|
120
|
+
steps=self.steps,
|
|
121
|
+
stream_processor=self.stream_processor,
|
|
122
|
+
console=self.console,
|
|
123
|
+
cfg=self.cfg,
|
|
124
|
+
step_server_start_times=self._step_server_start_times,
|
|
125
|
+
output_prefix="**Output:**\n",
|
|
126
|
+
)
|
|
127
|
+
self.thinking_controller = ThinkingScopeController(
|
|
128
|
+
self.steps,
|
|
129
|
+
step_server_start_times=self._step_server_start_times,
|
|
130
|
+
)
|
|
202
131
|
self._root_agent_friendly: str | None = None
|
|
203
132
|
self._root_agent_step_id: str | None = None
|
|
204
133
|
self._root_query: str | None = None
|
|
@@ -210,25 +139,11 @@ class RichStreamRenderer:
|
|
|
210
139
|
# Header/text
|
|
211
140
|
self.header_text: str = ""
|
|
212
141
|
# Track per-step server start times for accurate elapsed labels
|
|
213
|
-
self._step_server_start_times: dict[str, float] = {}
|
|
214
|
-
|
|
215
142
|
# Output formatting constants
|
|
216
143
|
self.OUTPUT_PREFIX: str = "**Output:**\n"
|
|
217
144
|
|
|
218
|
-
|
|
219
|
-
self.
|
|
220
|
-
self._transcript_render_cursor: int = 0
|
|
221
|
-
self.transcript_controller: Any | None = None
|
|
222
|
-
self._transcript_hint_message = (
|
|
223
|
-
"[dim]Transcript view · Press Ctrl+T to return to the summary.[/dim]"
|
|
224
|
-
)
|
|
225
|
-
self._summary_hint_message = (
|
|
226
|
-
"[dim]Press Ctrl+T to inspect raw transcript events.[/dim]"
|
|
227
|
-
)
|
|
228
|
-
self._summary_hint_printed_once: bool = False
|
|
229
|
-
self._transcript_hint_printed_once: bool = False
|
|
230
|
-
self._transcript_header_printed: bool = False
|
|
231
|
-
self._transcript_enabled_message_printed: bool = False
|
|
145
|
+
self._final_transcript_snapshot: TranscriptSnapshot | None = None
|
|
146
|
+
self._final_transcript_renderables: tuple[list[Any], list[Any]] | None = None
|
|
232
147
|
|
|
233
148
|
def on_start(self, meta: dict[str, Any]) -> None:
|
|
234
149
|
"""Handle renderer start event."""
|
|
@@ -245,10 +160,8 @@ class RichStreamRenderer:
|
|
|
245
160
|
|
|
246
161
|
meta_payload = meta or {}
|
|
247
162
|
self.steps.set_root_agent(meta_payload.get("agent_id"))
|
|
248
|
-
self._root_agent_friendly = self._humanize_agent_slug(
|
|
249
|
-
|
|
250
|
-
)
|
|
251
|
-
self._root_query = _truncate_display(
|
|
163
|
+
self._root_agent_friendly = self._humanize_agent_slug(meta_payload.get("agent_name"))
|
|
164
|
+
self._root_query = truncate_display(
|
|
252
165
|
meta_payload.get("input_message")
|
|
253
166
|
or meta_payload.get("query")
|
|
254
167
|
or meta_payload.get("message")
|
|
@@ -308,20 +221,6 @@ class RichStreamRenderer:
|
|
|
308
221
|
except Exception:
|
|
309
222
|
logger.exception("Failed to print header fallback")
|
|
310
223
|
|
|
311
|
-
def _extract_query_from_meta(self, meta: dict[str, Any] | None) -> str | None:
|
|
312
|
-
"""Extract the primary query string from a metadata payload."""
|
|
313
|
-
if not meta:
|
|
314
|
-
return None
|
|
315
|
-
query = (
|
|
316
|
-
meta.get("input_message")
|
|
317
|
-
or meta.get("query")
|
|
318
|
-
or meta.get("message")
|
|
319
|
-
or (meta.get("meta") or {}).get("input_message")
|
|
320
|
-
)
|
|
321
|
-
if isinstance(query, str) and query.strip():
|
|
322
|
-
return query
|
|
323
|
-
return None
|
|
324
|
-
|
|
325
224
|
def _build_user_query_panel(self, query: str) -> AIPPanel:
|
|
326
225
|
"""Create the panel used to display the user request."""
|
|
327
226
|
return AIPPanel(
|
|
@@ -333,7 +232,7 @@ class RichStreamRenderer:
|
|
|
333
232
|
|
|
334
233
|
def _render_user_query(self, meta: dict[str, Any]) -> None:
|
|
335
234
|
"""Render the user query panel."""
|
|
336
|
-
query =
|
|
235
|
+
query = extract_query_from_meta(meta)
|
|
337
236
|
if not query:
|
|
338
237
|
return
|
|
339
238
|
self.console.print(self._build_user_query_panel(query))
|
|
@@ -346,13 +245,42 @@ class RichStreamRenderer:
|
|
|
346
245
|
elif self.header_text and not self._render_header_rule():
|
|
347
246
|
self._render_header_fallback()
|
|
348
247
|
|
|
349
|
-
query =
|
|
248
|
+
query = extract_query_from_meta(meta) or self._root_query
|
|
350
249
|
if query:
|
|
351
250
|
self.console.print(self._build_user_query_panel(query))
|
|
352
251
|
|
|
252
|
+
def _render_summary_after_transcript_toggle(self) -> None:
|
|
253
|
+
"""Render the summary panel after leaving transcript mode."""
|
|
254
|
+
if self.state.finalizing_ui:
|
|
255
|
+
self._render_final_summary_panels()
|
|
256
|
+
elif self.live:
|
|
257
|
+
self._refresh_live_panels()
|
|
258
|
+
else:
|
|
259
|
+
self._render_static_summary_panels()
|
|
260
|
+
|
|
261
|
+
def _render_final_summary_panels(self) -> None:
|
|
262
|
+
"""Render a static summary and disable live mode for final output."""
|
|
263
|
+
self.cfg.live = False
|
|
264
|
+
self.live = None
|
|
265
|
+
self._render_static_summary_panels()
|
|
266
|
+
|
|
267
|
+
def _render_static_summary_panels(self) -> None:
|
|
268
|
+
"""Render the steps and main panels in a static (non-live) layout."""
|
|
269
|
+
summary_window = self._summary_window_size()
|
|
270
|
+
window_arg = summary_window if summary_window > 0 else None
|
|
271
|
+
status_overrides = self._build_step_status_overrides()
|
|
272
|
+
for renderable in render_summary_panels(
|
|
273
|
+
self.state,
|
|
274
|
+
self.steps,
|
|
275
|
+
summary_window=window_arg,
|
|
276
|
+
include_query_panel=False,
|
|
277
|
+
step_status_overrides=status_overrides,
|
|
278
|
+
):
|
|
279
|
+
self.console.print(renderable)
|
|
280
|
+
|
|
353
281
|
def _ensure_streaming_started_baseline(self, timestamp: float) -> None:
|
|
354
282
|
"""Synchronize streaming start state across renderer components."""
|
|
355
|
-
self.state.
|
|
283
|
+
self.state.start_stream_timer(timestamp)
|
|
356
284
|
self.stream_processor.streaming_started_at = timestamp
|
|
357
285
|
self._started_at = timestamp
|
|
358
286
|
|
|
@@ -374,7 +302,7 @@ class RichStreamRenderer:
|
|
|
374
302
|
|
|
375
303
|
def _resolve_received_timestamp(self, ev: dict[str, Any]) -> datetime:
|
|
376
304
|
"""Return the timestamp an event was received, normalising inputs."""
|
|
377
|
-
received_at =
|
|
305
|
+
received_at = coerce_received_at(ev.get("received_at"))
|
|
378
306
|
if received_at is None:
|
|
379
307
|
received_at = datetime.now(timezone.utc)
|
|
380
308
|
|
|
@@ -383,9 +311,7 @@ class RichStreamRenderer:
|
|
|
383
311
|
|
|
384
312
|
return received_at
|
|
385
313
|
|
|
386
|
-
def _sync_stream_start(
|
|
387
|
-
self, ev: dict[str, Any], received_at: datetime | None
|
|
388
|
-
) -> None:
|
|
314
|
+
def _sync_stream_start(self, ev: dict[str, Any], received_at: datetime | None) -> None:
|
|
389
315
|
"""Ensure renderer and stream processor share a streaming baseline."""
|
|
390
316
|
baseline = self.state.streaming_started_at
|
|
391
317
|
if baseline is None:
|
|
@@ -439,50 +365,37 @@ class RichStreamRenderer:
|
|
|
439
365
|
def _handle_content_event(self, content: str) -> None:
|
|
440
366
|
"""Handle content streaming events."""
|
|
441
367
|
if content:
|
|
442
|
-
self.state.
|
|
368
|
+
self.state.append_transcript_text(content)
|
|
443
369
|
self._ensure_live()
|
|
444
370
|
|
|
445
|
-
def _handle_final_response_event(
|
|
446
|
-
self, content: str, metadata: dict[str, Any]
|
|
447
|
-
) -> None:
|
|
371
|
+
def _handle_final_response_event(self, content: str, metadata: dict[str, Any]) -> None:
|
|
448
372
|
"""Handle final response events."""
|
|
449
373
|
if content:
|
|
450
|
-
self.state.
|
|
451
|
-
self.state.
|
|
374
|
+
self.state.append_transcript_text(content)
|
|
375
|
+
self.state.set_final_output(content)
|
|
452
376
|
|
|
453
377
|
meta_payload = metadata.get("metadata") or {}
|
|
454
|
-
final_time =
|
|
378
|
+
final_time = coerce_server_time(meta_payload.get("time"))
|
|
455
379
|
self._update_final_duration(final_time)
|
|
456
|
-
self.
|
|
380
|
+
self.thinking_controller.close_active_scopes(final_time)
|
|
457
381
|
self._finish_running_steps()
|
|
458
|
-
self.
|
|
382
|
+
self.tool_controller.finish_all_panels()
|
|
459
383
|
self._normalise_finished_icons()
|
|
460
384
|
|
|
461
385
|
self._ensure_live()
|
|
462
386
|
self._print_final_panel_if_needed()
|
|
463
387
|
|
|
464
388
|
def _normalise_finished_icons(self) -> None:
|
|
465
|
-
"""Ensure finished steps
|
|
389
|
+
"""Ensure finished steps release any running spinners."""
|
|
466
390
|
for step in self.steps.by_id.values():
|
|
467
|
-
if (
|
|
468
|
-
getattr(step, "status", None) == "finished"
|
|
469
|
-
and getattr(step, "status_icon", None) == "spinner"
|
|
470
|
-
):
|
|
471
|
-
step.status_icon = "success"
|
|
472
391
|
if getattr(step, "status", None) != "running":
|
|
473
392
|
self._step_spinners.pop(step.step_id, None)
|
|
474
393
|
|
|
475
|
-
def _handle_agent_step_event(
|
|
476
|
-
self, ev: dict[str, Any], metadata: dict[str, Any]
|
|
477
|
-
) -> None:
|
|
394
|
+
def _handle_agent_step_event(self, ev: dict[str, Any], metadata: dict[str, Any]) -> None:
|
|
478
395
|
"""Handle agent step events."""
|
|
479
|
-
# Extract tool information
|
|
480
|
-
(
|
|
481
|
-
|
|
482
|
-
tool_args,
|
|
483
|
-
tool_out,
|
|
484
|
-
tool_calls_info,
|
|
485
|
-
) = self.stream_processor.parse_tool_calls(ev)
|
|
396
|
+
# Extract tool information using stream processor
|
|
397
|
+
tool_calls_result = self.stream_processor.parse_tool_calls(ev)
|
|
398
|
+
tool_name, tool_args, tool_out, tool_calls_info = tool_calls_result
|
|
486
399
|
|
|
487
400
|
payload = metadata.get("metadata") or {}
|
|
488
401
|
|
|
@@ -493,17 +406,19 @@ class RichStreamRenderer:
|
|
|
493
406
|
logger.debug("Malformed step event skipped", exc_info=True)
|
|
494
407
|
else:
|
|
495
408
|
self._record_step_server_start(tracked_step, payload)
|
|
496
|
-
self.
|
|
409
|
+
self.thinking_controller.update_timeline(
|
|
410
|
+
tracked_step,
|
|
411
|
+
payload,
|
|
412
|
+
enabled=self.cfg.render_thinking,
|
|
413
|
+
)
|
|
497
414
|
self._maybe_override_root_agent_label(tracked_step, payload)
|
|
498
415
|
self._maybe_attach_root_query(tracked_step)
|
|
499
416
|
|
|
500
417
|
# Track tools and sub-agents for transcript/debug context
|
|
501
|
-
self.stream_processor.track_tools_and_agents(
|
|
502
|
-
tool_name, tool_calls_info, is_delegation_tool
|
|
503
|
-
)
|
|
418
|
+
self.stream_processor.track_tools_and_agents(tool_name, tool_calls_info, is_delegation_tool)
|
|
504
419
|
|
|
505
420
|
# Handle tool execution
|
|
506
|
-
self.
|
|
421
|
+
self.tool_controller.handle_agent_step(
|
|
507
422
|
ev,
|
|
508
423
|
tool_name,
|
|
509
424
|
tool_args,
|
|
@@ -517,13 +432,7 @@ class RichStreamRenderer:
|
|
|
517
432
|
|
|
518
433
|
def _maybe_attach_root_query(self, step: Step | None) -> None:
|
|
519
434
|
"""Attach the user query to the root agent step for display."""
|
|
520
|
-
if
|
|
521
|
-
not step
|
|
522
|
-
or self._root_query_attached
|
|
523
|
-
or not self._root_query
|
|
524
|
-
or step.kind != "agent"
|
|
525
|
-
or step.parent_id
|
|
526
|
-
):
|
|
435
|
+
if not step or self._root_query_attached or not self._root_query or step.kind != "agent" or step.parent_id:
|
|
527
436
|
return
|
|
528
437
|
|
|
529
438
|
args = dict(getattr(step, "args", {}) or {})
|
|
@@ -531,9 +440,7 @@ class RichStreamRenderer:
|
|
|
531
440
|
step.args = args
|
|
532
441
|
self._root_query_attached = True
|
|
533
442
|
|
|
534
|
-
def _record_step_server_start(
|
|
535
|
-
self, step: Step | None, payload: dict[str, Any]
|
|
536
|
-
) -> None:
|
|
443
|
+
def _record_step_server_start(self, step: Step | None, payload: dict[str, Any]) -> None:
|
|
537
444
|
"""Store server-provided start times for elapsed calculations."""
|
|
538
445
|
if not step:
|
|
539
446
|
return
|
|
@@ -542,276 +449,21 @@ class RichStreamRenderer:
|
|
|
542
449
|
return
|
|
543
450
|
self._step_server_start_times.setdefault(step.step_id, float(server_time))
|
|
544
451
|
|
|
545
|
-
def _maybe_override_root_agent_label(
|
|
546
|
-
self, step: Step | None, payload: dict[str, Any]
|
|
547
|
-
) -> None:
|
|
452
|
+
def _maybe_override_root_agent_label(self, step: Step | None, payload: dict[str, Any]) -> None:
|
|
548
453
|
"""Ensure the root agent row uses the human-friendly name and shows the ID."""
|
|
549
454
|
if not step or step.kind != "agent" or step.parent_id:
|
|
550
455
|
return
|
|
551
|
-
friendly = self._root_agent_friendly or self._humanize_agent_slug(
|
|
552
|
-
(payload or {}).get("agent_name")
|
|
553
|
-
)
|
|
456
|
+
friendly = self._root_agent_friendly or self._humanize_agent_slug((payload or {}).get("agent_name"))
|
|
554
457
|
if not friendly:
|
|
555
458
|
return
|
|
556
459
|
agent_identifier = step.name or step.step_id
|
|
557
460
|
if not agent_identifier:
|
|
558
461
|
return
|
|
559
|
-
step.display_label = normalise_display_label(
|
|
560
|
-
f"{ICON_AGENT} {friendly} ({agent_identifier})"
|
|
561
|
-
)
|
|
462
|
+
step.display_label = normalise_display_label(f"{ICON_AGENT} {friendly} ({agent_identifier})")
|
|
562
463
|
if not self._root_agent_step_id:
|
|
563
464
|
self._root_agent_step_id = step.step_id
|
|
564
465
|
|
|
565
|
-
|
|
566
|
-
self, step: Step | None, payload: dict[str, Any]
|
|
567
|
-
) -> None:
|
|
568
|
-
"""Maintain deterministic thinking spans for each agent/delegate scope."""
|
|
569
|
-
if not self.cfg.render_thinking or not step:
|
|
570
|
-
return
|
|
571
|
-
|
|
572
|
-
now_monotonic = monotonic()
|
|
573
|
-
server_time = self._coerce_server_time(payload.get("time"))
|
|
574
|
-
status_hint = (payload.get("status") or "").lower()
|
|
575
|
-
|
|
576
|
-
if self._is_scope_anchor(step):
|
|
577
|
-
self._update_anchor_thinking(
|
|
578
|
-
step=step,
|
|
579
|
-
server_time=server_time,
|
|
580
|
-
status_hint=status_hint,
|
|
581
|
-
now_monotonic=now_monotonic,
|
|
582
|
-
)
|
|
583
|
-
return
|
|
584
|
-
|
|
585
|
-
self._update_child_thinking(
|
|
586
|
-
step=step,
|
|
587
|
-
server_time=server_time,
|
|
588
|
-
status_hint=status_hint,
|
|
589
|
-
now_monotonic=now_monotonic,
|
|
590
|
-
)
|
|
591
|
-
|
|
592
|
-
def _update_anchor_thinking(
|
|
593
|
-
self,
|
|
594
|
-
*,
|
|
595
|
-
step: Step,
|
|
596
|
-
server_time: float | None,
|
|
597
|
-
status_hint: str,
|
|
598
|
-
now_monotonic: float,
|
|
599
|
-
) -> None:
|
|
600
|
-
"""Handle deterministic thinking bookkeeping for agent/delegate anchors."""
|
|
601
|
-
scope = self._get_or_create_scope(step)
|
|
602
|
-
if scope.anchor_started_at is None and server_time is not None:
|
|
603
|
-
scope.anchor_started_at = server_time
|
|
604
|
-
|
|
605
|
-
if not scope.closed and scope.active_thinking_id is None:
|
|
606
|
-
self._start_scope_thinking(
|
|
607
|
-
scope,
|
|
608
|
-
start_server_time=scope.anchor_started_at or server_time,
|
|
609
|
-
start_monotonic=now_monotonic,
|
|
610
|
-
)
|
|
611
|
-
|
|
612
|
-
is_anchor_finished = status_hint in FINISHED_STATUS_HINTS or (
|
|
613
|
-
not status_hint and is_step_finished(step)
|
|
614
|
-
)
|
|
615
|
-
if is_anchor_finished:
|
|
616
|
-
scope.anchor_finished_at = server_time or scope.anchor_finished_at
|
|
617
|
-
self._finish_scope_thinking(scope, server_time, now_monotonic)
|
|
618
|
-
scope.closed = True
|
|
619
|
-
|
|
620
|
-
parent_anchor_id = self._resolve_anchor_id(step)
|
|
621
|
-
if parent_anchor_id:
|
|
622
|
-
self._cascade_anchor_update(
|
|
623
|
-
parent_anchor_id=parent_anchor_id,
|
|
624
|
-
child_step=step,
|
|
625
|
-
server_time=server_time,
|
|
626
|
-
now_monotonic=now_monotonic,
|
|
627
|
-
is_finished=is_anchor_finished,
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
def _cascade_anchor_update(
|
|
631
|
-
self,
|
|
632
|
-
*,
|
|
633
|
-
parent_anchor_id: str,
|
|
634
|
-
child_step: Step,
|
|
635
|
-
server_time: float | None,
|
|
636
|
-
now_monotonic: float,
|
|
637
|
-
is_finished: bool,
|
|
638
|
-
) -> None:
|
|
639
|
-
"""Propagate anchor state changes to the parent scope."""
|
|
640
|
-
parent_scope = self._thinking_scopes.get(parent_anchor_id)
|
|
641
|
-
if not parent_scope or parent_scope.closed:
|
|
642
|
-
return
|
|
643
|
-
if is_finished:
|
|
644
|
-
self._mark_child_finished(
|
|
645
|
-
parent_scope, child_step.step_id, server_time, now_monotonic
|
|
646
|
-
)
|
|
647
|
-
else:
|
|
648
|
-
self._mark_child_running(
|
|
649
|
-
parent_scope, child_step, server_time, now_monotonic
|
|
650
|
-
)
|
|
651
|
-
|
|
652
|
-
def _update_child_thinking(
|
|
653
|
-
self,
|
|
654
|
-
*,
|
|
655
|
-
step: Step,
|
|
656
|
-
server_time: float | None,
|
|
657
|
-
status_hint: str,
|
|
658
|
-
now_monotonic: float,
|
|
659
|
-
) -> None:
|
|
660
|
-
"""Update deterministic thinking state for non-anchor steps."""
|
|
661
|
-
anchor_id = self._resolve_anchor_id(step)
|
|
662
|
-
if not anchor_id:
|
|
663
|
-
return
|
|
664
|
-
|
|
665
|
-
scope = self._thinking_scopes.get(anchor_id)
|
|
666
|
-
if not scope or scope.closed or step.kind == "thinking":
|
|
667
|
-
return
|
|
668
|
-
|
|
669
|
-
is_finish_event = status_hint in FINISHED_STATUS_HINTS or (
|
|
670
|
-
not status_hint and is_step_finished(step)
|
|
671
|
-
)
|
|
672
|
-
if is_finish_event:
|
|
673
|
-
self._mark_child_finished(scope, step.step_id, server_time, now_monotonic)
|
|
674
|
-
else:
|
|
675
|
-
self._mark_child_running(scope, step, server_time, now_monotonic)
|
|
676
|
-
|
|
677
|
-
def _resolve_anchor_id(self, step: Step) -> str | None:
|
|
678
|
-
"""Return the nearest agent/delegate ancestor for a step."""
|
|
679
|
-
parent_id = step.parent_id
|
|
680
|
-
while parent_id:
|
|
681
|
-
parent = self.steps.by_id.get(parent_id)
|
|
682
|
-
if not parent:
|
|
683
|
-
return None
|
|
684
|
-
if self._is_scope_anchor(parent):
|
|
685
|
-
return parent.step_id
|
|
686
|
-
parent_id = parent.parent_id
|
|
687
|
-
return None
|
|
688
|
-
|
|
689
|
-
def _get_or_create_scope(self, step: Step) -> ThinkingScopeState:
|
|
690
|
-
"""Fetch (or create) thinking state for the given anchor step."""
|
|
691
|
-
scope = self._thinking_scopes.get(step.step_id)
|
|
692
|
-
if scope:
|
|
693
|
-
if scope.task_id is None:
|
|
694
|
-
scope.task_id = step.task_id
|
|
695
|
-
if scope.context_id is None:
|
|
696
|
-
scope.context_id = step.context_id
|
|
697
|
-
return scope
|
|
698
|
-
scope = ThinkingScopeState(
|
|
699
|
-
anchor_id=step.step_id,
|
|
700
|
-
task_id=step.task_id,
|
|
701
|
-
context_id=step.context_id,
|
|
702
|
-
)
|
|
703
|
-
self._thinking_scopes[step.step_id] = scope
|
|
704
|
-
return scope
|
|
705
|
-
|
|
706
|
-
def _is_scope_anchor(self, step: Step) -> bool:
|
|
707
|
-
"""Return True when a step should host its own thinking timeline."""
|
|
708
|
-
if step.kind in {"agent", "delegate"}:
|
|
709
|
-
return True
|
|
710
|
-
name = (step.name or "").lower()
|
|
711
|
-
return name.startswith(("delegate_to_", "delegate_", "delegate "))
|
|
712
|
-
|
|
713
|
-
def _start_scope_thinking(
|
|
714
|
-
self,
|
|
715
|
-
scope: ThinkingScopeState,
|
|
716
|
-
*,
|
|
717
|
-
start_server_time: float | None,
|
|
718
|
-
start_monotonic: float,
|
|
719
|
-
) -> None:
|
|
720
|
-
"""Open a deterministic thinking node beneath the scope anchor."""
|
|
721
|
-
if scope.closed or scope.active_thinking_id or not scope.anchor_id:
|
|
722
|
-
return
|
|
723
|
-
step = self.steps.start_or_get(
|
|
724
|
-
task_id=scope.task_id,
|
|
725
|
-
context_id=scope.context_id,
|
|
726
|
-
kind="thinking",
|
|
727
|
-
name=f"agent_thinking_step::{scope.anchor_id}",
|
|
728
|
-
parent_id=scope.anchor_id,
|
|
729
|
-
args={"reason": "deterministic_timeline"},
|
|
730
|
-
)
|
|
731
|
-
step.display_label = "💭 Thinking…"
|
|
732
|
-
step.status_icon = "spinner"
|
|
733
|
-
scope.active_thinking_id = step.step_id
|
|
734
|
-
scope.idle_started_at = start_server_time
|
|
735
|
-
scope.idle_started_monotonic = start_monotonic
|
|
736
|
-
|
|
737
|
-
def _finish_scope_thinking(
|
|
738
|
-
self,
|
|
739
|
-
scope: ThinkingScopeState,
|
|
740
|
-
end_server_time: float | None,
|
|
741
|
-
end_monotonic: float,
|
|
742
|
-
) -> None:
|
|
743
|
-
"""Close the currently running thinking node if one exists."""
|
|
744
|
-
if not scope.active_thinking_id:
|
|
745
|
-
return
|
|
746
|
-
thinking_step = self.steps.by_id.get(scope.active_thinking_id)
|
|
747
|
-
if not thinking_step:
|
|
748
|
-
scope.active_thinking_id = None
|
|
749
|
-
scope.idle_started_at = None
|
|
750
|
-
scope.idle_started_monotonic = None
|
|
751
|
-
return
|
|
752
|
-
|
|
753
|
-
duration = self._calculate_timeline_duration(
|
|
754
|
-
scope.idle_started_at,
|
|
755
|
-
end_server_time,
|
|
756
|
-
scope.idle_started_monotonic,
|
|
757
|
-
end_monotonic,
|
|
758
|
-
)
|
|
759
|
-
thinking_step.display_label = thinking_step.display_label or "💭 Thinking…"
|
|
760
|
-
if duration is not None:
|
|
761
|
-
thinking_step.finish(duration, source="timeline")
|
|
762
|
-
else:
|
|
763
|
-
thinking_step.finish(None, source="timeline")
|
|
764
|
-
thinking_step.status_icon = "success"
|
|
765
|
-
scope.active_thinking_id = None
|
|
766
|
-
scope.idle_started_at = None
|
|
767
|
-
scope.idle_started_monotonic = None
|
|
768
|
-
|
|
769
|
-
def _mark_child_running(
|
|
770
|
-
self,
|
|
771
|
-
scope: ThinkingScopeState,
|
|
772
|
-
step: Step,
|
|
773
|
-
server_time: float | None,
|
|
774
|
-
now_monotonic: float,
|
|
775
|
-
) -> None:
|
|
776
|
-
"""Mark a direct child as running and close any open thinking node."""
|
|
777
|
-
if step.step_id in scope.running_children:
|
|
778
|
-
return
|
|
779
|
-
scope.running_children.add(step.step_id)
|
|
780
|
-
if not scope.active_thinking_id:
|
|
781
|
-
return
|
|
782
|
-
|
|
783
|
-
start_server = self._step_server_start_times.get(step.step_id)
|
|
784
|
-
if start_server is None:
|
|
785
|
-
start_server = server_time
|
|
786
|
-
self._finish_scope_thinking(scope, start_server, now_monotonic)
|
|
787
|
-
|
|
788
|
-
def _mark_child_finished(
|
|
789
|
-
self,
|
|
790
|
-
scope: ThinkingScopeState,
|
|
791
|
-
step_id: str,
|
|
792
|
-
server_time: float | None,
|
|
793
|
-
now_monotonic: float,
|
|
794
|
-
) -> None:
|
|
795
|
-
"""Handle completion for a scope child and resume thinking if idle."""
|
|
796
|
-
if step_id in scope.running_children:
|
|
797
|
-
scope.running_children.discard(step_id)
|
|
798
|
-
if scope.running_children or scope.closed:
|
|
799
|
-
return
|
|
800
|
-
self._start_scope_thinking(
|
|
801
|
-
scope,
|
|
802
|
-
start_server_time=server_time,
|
|
803
|
-
start_monotonic=now_monotonic,
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
def _close_active_thinking_scopes(self, server_time: float | None) -> None:
|
|
807
|
-
"""Finish any in-flight thinking nodes during finalization."""
|
|
808
|
-
now = monotonic()
|
|
809
|
-
for scope in self._thinking_scopes.values():
|
|
810
|
-
if not scope.active_thinking_id:
|
|
811
|
-
continue
|
|
812
|
-
self._finish_scope_thinking(scope, server_time, now)
|
|
813
|
-
scope.closed = True
|
|
814
|
-
# Parent scopes resume thinking via _cascade_anchor_update
|
|
466
|
+
# Thinking scope management is handled by ThinkingScopeController.
|
|
815
467
|
|
|
816
468
|
def _apply_root_duration(self, duration_seconds: float | None) -> None:
|
|
817
469
|
"""Propagate the final run duration to the root agent step."""
|
|
@@ -828,33 +480,6 @@ class RichStreamRenderer:
|
|
|
828
480
|
root_step.duration_source = root_step.duration_source or "run"
|
|
829
481
|
root_step.status = "finished"
|
|
830
482
|
|
|
831
|
-
@staticmethod
|
|
832
|
-
def _coerce_server_time(value: Any) -> float | None:
|
|
833
|
-
"""Convert a raw SSE time payload into a float if possible."""
|
|
834
|
-
if isinstance(value, (int, float)):
|
|
835
|
-
return float(value)
|
|
836
|
-
try:
|
|
837
|
-
return float(value)
|
|
838
|
-
except (TypeError, ValueError):
|
|
839
|
-
return None
|
|
840
|
-
|
|
841
|
-
@staticmethod
|
|
842
|
-
def _calculate_timeline_duration(
|
|
843
|
-
start_server: float | None,
|
|
844
|
-
end_server: float | None,
|
|
845
|
-
start_monotonic: float | None,
|
|
846
|
-
end_monotonic: float,
|
|
847
|
-
) -> float | None:
|
|
848
|
-
"""Pick the most reliable pair of timestamps to derive duration seconds."""
|
|
849
|
-
if start_server is not None and end_server is not None:
|
|
850
|
-
return max(0.0, float(end_server) - float(start_server))
|
|
851
|
-
if start_monotonic is not None:
|
|
852
|
-
try:
|
|
853
|
-
return max(0.0, float(end_monotonic) - float(start_monotonic))
|
|
854
|
-
except Exception:
|
|
855
|
-
return None
|
|
856
|
-
return None
|
|
857
|
-
|
|
858
483
|
@staticmethod
|
|
859
484
|
def _humanize_agent_slug(value: Any) -> str | None:
|
|
860
485
|
"""Convert a slugified agent name into Title Case."""
|
|
@@ -879,19 +504,6 @@ class RichStreamRenderer:
|
|
|
879
504
|
if step.duration_ms is None:
|
|
880
505
|
step.duration_ms = 0
|
|
881
506
|
step.duration_source = step.duration_source or "unknown"
|
|
882
|
-
step.status_icon = "warning"
|
|
883
|
-
|
|
884
|
-
def _finish_tool_panels(self) -> None:
|
|
885
|
-
"""Mark unfinished tool panels as finished."""
|
|
886
|
-
try:
|
|
887
|
-
items = list(self.tool_panels.items())
|
|
888
|
-
except Exception: # pragma: no cover - defensive guard
|
|
889
|
-
logger.exception("Failed to iterate tool panels during cleanup")
|
|
890
|
-
return
|
|
891
|
-
|
|
892
|
-
for _sid, meta in items:
|
|
893
|
-
if meta.get("status") != "finished":
|
|
894
|
-
meta["status"] = "finished"
|
|
895
507
|
|
|
896
508
|
def _stop_live_display(self) -> None:
|
|
897
509
|
"""Stop live display and clean up."""
|
|
@@ -902,7 +514,7 @@ class RichStreamRenderer:
|
|
|
902
514
|
if self.state.printed_final_output:
|
|
903
515
|
return
|
|
904
516
|
|
|
905
|
-
body = (self.state.final_text or
|
|
517
|
+
body = (self.state.final_text or self.state.buffer.render() or "").strip()
|
|
906
518
|
if not body:
|
|
907
519
|
return
|
|
908
520
|
|
|
@@ -910,20 +522,52 @@ class RichStreamRenderer:
|
|
|
910
522
|
return
|
|
911
523
|
|
|
912
524
|
if self.verbose:
|
|
913
|
-
|
|
914
|
-
|
|
525
|
+
panel = build_final_panel(
|
|
526
|
+
self.state,
|
|
915
527
|
title=self._final_panel_title(),
|
|
916
|
-
theme=self.cfg.theme,
|
|
917
528
|
)
|
|
918
|
-
|
|
529
|
+
if panel is None:
|
|
530
|
+
return
|
|
531
|
+
self.console.print(panel)
|
|
919
532
|
self.state.printed_final_output = True
|
|
920
533
|
|
|
534
|
+
def finalize(self) -> tuple[list[Any], list[Any]]:
|
|
535
|
+
"""Compose the final transcript renderables."""
|
|
536
|
+
return self._compose_final_transcript()
|
|
537
|
+
|
|
538
|
+
def _compose_final_transcript(self) -> tuple[list[Any], list[Any]]:
|
|
539
|
+
"""Build the transcript snapshot used for final summaries."""
|
|
540
|
+
summary_window = self._summary_window_size()
|
|
541
|
+
summary_window = summary_window if summary_window > 0 else None
|
|
542
|
+
snapshot = build_transcript_snapshot(
|
|
543
|
+
self.state,
|
|
544
|
+
self.steps,
|
|
545
|
+
query_text=extract_query_from_meta(self.state.meta),
|
|
546
|
+
meta=self.state.meta,
|
|
547
|
+
summary_window=summary_window,
|
|
548
|
+
step_status_overrides=self._build_step_status_overrides(),
|
|
549
|
+
)
|
|
550
|
+
header, body = build_transcript_view(snapshot)
|
|
551
|
+
self._final_transcript_snapshot = snapshot
|
|
552
|
+
self._final_transcript_renderables = (header, body)
|
|
553
|
+
return header, body
|
|
554
|
+
|
|
555
|
+
def _render_final_summary(self, header: list[Any], body: list[Any]) -> None:
|
|
556
|
+
"""Print the composed transcript summary for non-live renders."""
|
|
557
|
+
renderables = list(header) + list(body)
|
|
558
|
+
for renderable in renderables:
|
|
559
|
+
try:
|
|
560
|
+
self.console.print(renderable)
|
|
561
|
+
self.console.print()
|
|
562
|
+
except Exception:
|
|
563
|
+
pass
|
|
564
|
+
|
|
921
565
|
def on_complete(self, stats: RunStats) -> None:
|
|
922
566
|
"""Handle completion event."""
|
|
923
567
|
self.state.finalizing_ui = True
|
|
924
568
|
|
|
925
569
|
self._handle_stats_duration(stats)
|
|
926
|
-
self.
|
|
570
|
+
self.thinking_controller.close_active_scopes(self.state.final_duration_seconds)
|
|
927
571
|
self._cleanup_ui_elements()
|
|
928
572
|
self._finalize_display()
|
|
929
573
|
self._print_completion_message()
|
|
@@ -949,25 +593,31 @@ class RichStreamRenderer:
|
|
|
949
593
|
self._finish_running_steps()
|
|
950
594
|
|
|
951
595
|
# Mark unfinished tool panels as finished
|
|
952
|
-
self.
|
|
596
|
+
self.tool_controller.finish_all_panels()
|
|
953
597
|
|
|
954
598
|
def _finalize_display(self) -> None:
|
|
955
599
|
"""Finalize live display and render final output."""
|
|
956
600
|
# Final refresh
|
|
957
601
|
self._ensure_live()
|
|
958
602
|
|
|
603
|
+
header, body = self.finalize()
|
|
604
|
+
|
|
959
605
|
# Stop live display
|
|
960
606
|
self._stop_live_display()
|
|
961
607
|
|
|
962
608
|
# Render final output based on configuration
|
|
963
|
-
self.
|
|
609
|
+
if self.cfg.live:
|
|
610
|
+
self._print_final_panel_if_needed()
|
|
611
|
+
else:
|
|
612
|
+
self._render_final_summary(header, body)
|
|
964
613
|
|
|
965
614
|
def _print_completion_message(self) -> None:
|
|
966
615
|
"""Print completion message based on current mode."""
|
|
967
616
|
if self._transcript_mode_enabled:
|
|
968
617
|
try:
|
|
969
618
|
self.console.print(
|
|
970
|
-
"[dim]Run finished. Press Ctrl+T to return to the summary view or stay here to inspect events.
|
|
619
|
+
"[dim]Run finished. Press Ctrl+T to return to the summary view or stay here to inspect events. "
|
|
620
|
+
"Use the post-run viewer for export.[/dim]"
|
|
971
621
|
)
|
|
972
622
|
except Exception:
|
|
973
623
|
pass
|
|
@@ -1031,13 +681,18 @@ class RichStreamRenderer:
|
|
|
1031
681
|
if not self.live:
|
|
1032
682
|
return
|
|
1033
683
|
|
|
1034
|
-
|
|
1035
|
-
|
|
684
|
+
steps_body = self._render_steps_text()
|
|
685
|
+
template_panel = getattr(self, "_last_steps_panel_template", None)
|
|
686
|
+
if template_panel is None:
|
|
687
|
+
template_panel = self._resolve_steps_panel()
|
|
1036
688
|
steps_panel = AIPPanel(
|
|
1037
|
-
|
|
1038
|
-
title="Steps",
|
|
1039
|
-
border_style="blue",
|
|
689
|
+
steps_body,
|
|
690
|
+
title=getattr(template_panel, "title", "Steps"),
|
|
691
|
+
border_style=getattr(template_panel, "border_style", "blue"),
|
|
692
|
+
padding=getattr(template_panel, "padding", (0, 1)),
|
|
1040
693
|
)
|
|
694
|
+
|
|
695
|
+
main_panel = self._render_main_panel()
|
|
1041
696
|
panels = self._build_live_panels(main_panel, steps_panel)
|
|
1042
697
|
|
|
1043
698
|
self.live.update(Group(*panels))
|
|
@@ -1055,26 +710,19 @@ class RichStreamRenderer:
|
|
|
1055
710
|
|
|
1056
711
|
def _render_main_panel(self) -> Any:
|
|
1057
712
|
"""Render the main content panel."""
|
|
1058
|
-
body =
|
|
713
|
+
body = self.state.buffer.render().strip()
|
|
714
|
+
theme = DEFAULT_TRANSCRIPT_THEME
|
|
1059
715
|
if not self.verbose:
|
|
1060
|
-
|
|
1061
|
-
if
|
|
1062
|
-
|
|
1063
|
-
return create_final_panel(
|
|
1064
|
-
final_content,
|
|
1065
|
-
title=title,
|
|
1066
|
-
theme=self.cfg.theme,
|
|
1067
|
-
)
|
|
716
|
+
panel = build_final_panel(self.state, theme=theme)
|
|
717
|
+
if panel is not None:
|
|
718
|
+
return panel
|
|
1068
719
|
# Dynamic title with spinner + elapsed/hints
|
|
1069
720
|
title = self._format_enhanced_main_title()
|
|
1070
|
-
return create_main_panel(body, title,
|
|
721
|
+
return create_main_panel(body, title, theme)
|
|
1071
722
|
|
|
1072
723
|
def _final_panel_title(self) -> str:
|
|
1073
724
|
"""Compose title for the final result panel including duration."""
|
|
1074
|
-
|
|
1075
|
-
if self.state.final_duration_text:
|
|
1076
|
-
title = f"{title} · {self.state.final_duration_text}"
|
|
1077
|
-
return title
|
|
725
|
+
return format_final_panel_title(self.state)
|
|
1078
726
|
|
|
1079
727
|
def apply_verbosity(self, verbose: bool) -> None:
|
|
1080
728
|
"""Update verbose behaviour at runtime."""
|
|
@@ -1082,8 +730,6 @@ class RichStreamRenderer:
|
|
|
1082
730
|
return
|
|
1083
731
|
|
|
1084
732
|
self.verbose = verbose
|
|
1085
|
-
self.cfg.style = "debug" if verbose else "pretty"
|
|
1086
|
-
|
|
1087
733
|
desired_live = not verbose
|
|
1088
734
|
if desired_live != self.cfg.live:
|
|
1089
735
|
self.cfg.live = desired_live
|
|
@@ -1095,604 +741,16 @@ class RichStreamRenderer:
|
|
|
1095
741
|
if self.cfg.live:
|
|
1096
742
|
self._ensure_live()
|
|
1097
743
|
|
|
1098
|
-
#
|
|
1099
|
-
# Transcript helpers
|
|
1100
|
-
# ------------------------------------------------------------------
|
|
1101
|
-
@property
|
|
1102
|
-
def transcript_mode_enabled(self) -> bool:
|
|
1103
|
-
"""Return True when transcript mode is currently active."""
|
|
1104
|
-
return self._transcript_mode_enabled
|
|
1105
|
-
|
|
1106
|
-
def toggle_transcript_mode(self) -> None:
|
|
1107
|
-
"""Flip transcript mode on/off."""
|
|
1108
|
-
self.set_transcript_mode(not self._transcript_mode_enabled)
|
|
1109
|
-
|
|
1110
|
-
def set_transcript_mode(self, enabled: bool) -> None:
|
|
1111
|
-
"""Set transcript mode explicitly."""
|
|
1112
|
-
if enabled == self._transcript_mode_enabled:
|
|
1113
|
-
return
|
|
1114
|
-
|
|
1115
|
-
self._transcript_mode_enabled = enabled
|
|
1116
|
-
self.apply_verbosity(enabled)
|
|
1117
|
-
|
|
1118
|
-
if enabled:
|
|
1119
|
-
self._summary_hint_printed_once = False
|
|
1120
|
-
self._transcript_hint_printed_once = False
|
|
1121
|
-
self._transcript_header_printed = False
|
|
1122
|
-
self._transcript_enabled_message_printed = False
|
|
1123
|
-
self._stop_live_display()
|
|
1124
|
-
self._clear_console_safe()
|
|
1125
|
-
self._print_transcript_enabled_message()
|
|
1126
|
-
self._render_transcript_backfill()
|
|
1127
|
-
else:
|
|
1128
|
-
self._transcript_hint_printed_once = False
|
|
1129
|
-
self._transcript_header_printed = False
|
|
1130
|
-
self._transcript_enabled_message_printed = False
|
|
1131
|
-
self._clear_console_safe()
|
|
1132
|
-
self._render_summary_static_sections()
|
|
1133
|
-
summary_notice = (
|
|
1134
|
-
"[dim]Returning to the summary view. Streaming will continue here.[/dim]"
|
|
1135
|
-
if not self.state.finalizing_ui
|
|
1136
|
-
else "[dim]Returning to the summary view.[/dim]"
|
|
1137
|
-
)
|
|
1138
|
-
self.console.print(summary_notice)
|
|
1139
|
-
if self.live:
|
|
1140
|
-
self._refresh_live_panels()
|
|
1141
|
-
else:
|
|
1142
|
-
steps_renderable = self._render_steps_text()
|
|
1143
|
-
steps_panel = AIPPanel(
|
|
1144
|
-
steps_renderable,
|
|
1145
|
-
title="Steps",
|
|
1146
|
-
border_style="blue",
|
|
1147
|
-
)
|
|
1148
|
-
self.console.print(steps_panel)
|
|
1149
|
-
self.console.print(self._render_main_panel())
|
|
1150
|
-
if not self.state.finalizing_ui:
|
|
1151
|
-
self._print_summary_hint(force=True)
|
|
1152
|
-
|
|
1153
|
-
def _clear_console_safe(self) -> None:
|
|
1154
|
-
"""Best-effort console clear that ignores platform quirks."""
|
|
1155
|
-
try:
|
|
1156
|
-
self.console.clear()
|
|
1157
|
-
except Exception:
|
|
1158
|
-
pass
|
|
1159
|
-
|
|
1160
|
-
def _print_transcript_hint(self) -> None:
|
|
1161
|
-
"""Render the transcript toggle hint, keeping it near the bottom."""
|
|
1162
|
-
if not self._transcript_mode_enabled:
|
|
1163
|
-
return
|
|
1164
|
-
try:
|
|
1165
|
-
self.console.print(self._transcript_hint_message)
|
|
1166
|
-
except Exception:
|
|
1167
|
-
pass
|
|
1168
|
-
else:
|
|
1169
|
-
self._transcript_hint_printed_once = True
|
|
1170
|
-
|
|
1171
|
-
def _print_transcript_enabled_message(self) -> None:
|
|
1172
|
-
if self._transcript_enabled_message_printed:
|
|
1173
|
-
return
|
|
1174
|
-
try:
|
|
1175
|
-
self.console.print(
|
|
1176
|
-
"[dim]Transcript mode enabled — streaming raw transcript events.[/dim]"
|
|
1177
|
-
)
|
|
1178
|
-
except Exception:
|
|
1179
|
-
pass
|
|
1180
|
-
else:
|
|
1181
|
-
self._transcript_enabled_message_printed = True
|
|
1182
|
-
|
|
1183
|
-
def _ensure_transcript_header(self) -> None:
|
|
1184
|
-
if self._transcript_header_printed:
|
|
1185
|
-
return
|
|
1186
|
-
try:
|
|
1187
|
-
self.console.rule("Transcript Events")
|
|
1188
|
-
except Exception:
|
|
1189
|
-
self._transcript_header_printed = True
|
|
1190
|
-
return
|
|
1191
|
-
self._transcript_header_printed = True
|
|
1192
|
-
|
|
1193
|
-
def _print_summary_hint(self, force: bool = False) -> None:
|
|
1194
|
-
"""Show the summary-mode toggle hint."""
|
|
1195
|
-
controller = getattr(self, "transcript_controller", None)
|
|
1196
|
-
if controller and not getattr(controller, "enabled", False):
|
|
1197
|
-
if not force:
|
|
1198
|
-
self._summary_hint_printed_once = True
|
|
1199
|
-
return
|
|
1200
|
-
if not force and self._summary_hint_printed_once:
|
|
1201
|
-
return
|
|
1202
|
-
try:
|
|
1203
|
-
self.console.print(self._summary_hint_message)
|
|
1204
|
-
except Exception:
|
|
1205
|
-
return
|
|
1206
|
-
self._summary_hint_printed_once = True
|
|
1207
|
-
|
|
1208
|
-
def _render_transcript_backfill(self) -> None:
|
|
1209
|
-
"""Render any captured events that haven't been shown in transcript mode."""
|
|
1210
|
-
pending = self.state.events[self._transcript_render_cursor :]
|
|
1211
|
-
self._ensure_transcript_header()
|
|
1212
|
-
if not pending:
|
|
1213
|
-
self._print_transcript_hint()
|
|
1214
|
-
return
|
|
1215
|
-
|
|
1216
|
-
baseline = self.state.streaming_started_event_ts
|
|
1217
|
-
for ev in pending:
|
|
1218
|
-
received_ts = _coerce_received_at(ev.get("received_at"))
|
|
1219
|
-
render_debug_event(
|
|
1220
|
-
ev,
|
|
1221
|
-
self.console,
|
|
1222
|
-
received_ts=received_ts,
|
|
1223
|
-
baseline_ts=baseline,
|
|
1224
|
-
)
|
|
1225
|
-
|
|
1226
|
-
self._transcript_render_cursor = len(self.state.events)
|
|
1227
|
-
self._print_transcript_hint()
|
|
1228
|
-
|
|
1229
|
-
def _capture_event(
|
|
1230
|
-
self, ev: dict[str, Any], received_at: datetime | None = None
|
|
1231
|
-
) -> None:
|
|
1232
|
-
"""Capture a deep copy of SSE events for transcript replay."""
|
|
1233
|
-
try:
|
|
1234
|
-
captured = json.loads(json.dumps(ev))
|
|
1235
|
-
except Exception:
|
|
1236
|
-
captured = ev
|
|
1237
|
-
|
|
1238
|
-
if received_at is not None:
|
|
1239
|
-
try:
|
|
1240
|
-
captured["received_at"] = received_at.isoformat()
|
|
1241
|
-
except Exception:
|
|
1242
|
-
try:
|
|
1243
|
-
captured["received_at"] = str(received_at)
|
|
1244
|
-
except Exception:
|
|
1245
|
-
captured["received_at"] = repr(received_at)
|
|
1246
|
-
|
|
1247
|
-
self.state.events.append(captured)
|
|
1248
|
-
if self._transcript_mode_enabled:
|
|
1249
|
-
self._transcript_render_cursor = len(self.state.events)
|
|
744
|
+
# Transcript helper implementations live in TranscriptModeMixin.
|
|
1250
745
|
|
|
1251
746
|
def get_aggregated_output(self) -> str:
|
|
1252
747
|
"""Return the concatenated assistant output collected so far."""
|
|
1253
|
-
return
|
|
748
|
+
return self.state.buffer.render().strip()
|
|
1254
749
|
|
|
1255
750
|
def get_transcript_events(self) -> list[dict[str, Any]]:
|
|
1256
751
|
"""Return captured SSE events."""
|
|
1257
752
|
return list(self.state.events)
|
|
1258
753
|
|
|
1259
|
-
def _ensure_tool_panel(
|
|
1260
|
-
self, name: str, args: Any, task_id: str, context_id: str
|
|
1261
|
-
) -> str:
|
|
1262
|
-
"""Ensure a tool panel exists and return its ID."""
|
|
1263
|
-
formatted_title = format_tool_title(name)
|
|
1264
|
-
is_delegation = is_delegation_tool(name)
|
|
1265
|
-
tool_sid = f"tool_{name}_{task_id}_{context_id}"
|
|
1266
|
-
|
|
1267
|
-
if tool_sid not in self.tool_panels:
|
|
1268
|
-
self.tool_panels[tool_sid] = {
|
|
1269
|
-
"title": formatted_title,
|
|
1270
|
-
"status": "running",
|
|
1271
|
-
"started_at": monotonic(),
|
|
1272
|
-
"server_started_at": self.stream_processor.server_elapsed_time,
|
|
1273
|
-
"chunks": [],
|
|
1274
|
-
"args": args or {},
|
|
1275
|
-
"output": None,
|
|
1276
|
-
"is_delegation": is_delegation,
|
|
1277
|
-
}
|
|
1278
|
-
# Add Args section once
|
|
1279
|
-
if args:
|
|
1280
|
-
try:
|
|
1281
|
-
args_content = (
|
|
1282
|
-
"**Args:**\n```json\n"
|
|
1283
|
-
+ json.dumps(args, indent=2)
|
|
1284
|
-
+ "\n```\n\n"
|
|
1285
|
-
)
|
|
1286
|
-
except Exception:
|
|
1287
|
-
args_content = f"**Args:**\n{args}\n\n"
|
|
1288
|
-
self.tool_panels[tool_sid]["chunks"].append(args_content)
|
|
1289
|
-
|
|
1290
|
-
return tool_sid
|
|
1291
|
-
|
|
1292
|
-
def _start_tool_step(
|
|
1293
|
-
self,
|
|
1294
|
-
task_id: str,
|
|
1295
|
-
context_id: str,
|
|
1296
|
-
tool_name: str,
|
|
1297
|
-
tool_args: Any,
|
|
1298
|
-
_tool_sid: str,
|
|
1299
|
-
*,
|
|
1300
|
-
tracked_step: Step | None = None,
|
|
1301
|
-
) -> Step | None:
|
|
1302
|
-
"""Start or get a step for a tool."""
|
|
1303
|
-
if tracked_step is not None:
|
|
1304
|
-
return tracked_step
|
|
1305
|
-
|
|
1306
|
-
if is_delegation_tool(tool_name):
|
|
1307
|
-
st = self.steps.start_or_get(
|
|
1308
|
-
task_id=task_id,
|
|
1309
|
-
context_id=context_id,
|
|
1310
|
-
kind="delegate",
|
|
1311
|
-
name=tool_name,
|
|
1312
|
-
args=tool_args,
|
|
1313
|
-
)
|
|
1314
|
-
else:
|
|
1315
|
-
st = self.steps.start_or_get(
|
|
1316
|
-
task_id=task_id,
|
|
1317
|
-
context_id=context_id,
|
|
1318
|
-
kind="tool",
|
|
1319
|
-
name=tool_name,
|
|
1320
|
-
args=tool_args,
|
|
1321
|
-
)
|
|
1322
|
-
|
|
1323
|
-
# Record server start time for this step if available
|
|
1324
|
-
if st and self.stream_processor.server_elapsed_time is not None:
|
|
1325
|
-
self._step_server_start_times[st.step_id] = (
|
|
1326
|
-
self.stream_processor.server_elapsed_time
|
|
1327
|
-
)
|
|
1328
|
-
|
|
1329
|
-
return st
|
|
1330
|
-
|
|
1331
|
-
def _process_additional_tool_calls(
|
|
1332
|
-
self,
|
|
1333
|
-
tool_calls_info: list[tuple[str, Any, Any]],
|
|
1334
|
-
tool_name: str,
|
|
1335
|
-
task_id: str,
|
|
1336
|
-
context_id: str,
|
|
1337
|
-
) -> None:
|
|
1338
|
-
"""Process additional tool calls to avoid duplicates."""
|
|
1339
|
-
for call_name, call_args, _ in tool_calls_info or []:
|
|
1340
|
-
if call_name and call_name != tool_name:
|
|
1341
|
-
self._process_single_tool_call(
|
|
1342
|
-
call_name, call_args, task_id, context_id
|
|
1343
|
-
)
|
|
1344
|
-
|
|
1345
|
-
def _process_single_tool_call(
|
|
1346
|
-
self, call_name: str, call_args: Any, task_id: str, context_id: str
|
|
1347
|
-
) -> None:
|
|
1348
|
-
"""Process a single additional tool call."""
|
|
1349
|
-
self._ensure_tool_panel(call_name, call_args, task_id, context_id)
|
|
1350
|
-
|
|
1351
|
-
st2 = self._create_step_for_tool_call(call_name, call_args, task_id, context_id)
|
|
1352
|
-
|
|
1353
|
-
if self.stream_processor.server_elapsed_time is not None and st2:
|
|
1354
|
-
self._step_server_start_times[st2.step_id] = (
|
|
1355
|
-
self.stream_processor.server_elapsed_time
|
|
1356
|
-
)
|
|
1357
|
-
|
|
1358
|
-
def _create_step_for_tool_call(
|
|
1359
|
-
self, call_name: str, call_args: Any, task_id: str, context_id: str
|
|
1360
|
-
) -> Any:
|
|
1361
|
-
"""Create appropriate step for tool call."""
|
|
1362
|
-
if is_delegation_tool(call_name):
|
|
1363
|
-
return self.steps.start_or_get(
|
|
1364
|
-
task_id=task_id,
|
|
1365
|
-
context_id=context_id,
|
|
1366
|
-
kind="delegate",
|
|
1367
|
-
name=call_name,
|
|
1368
|
-
args=call_args,
|
|
1369
|
-
)
|
|
1370
|
-
else:
|
|
1371
|
-
return self.steps.start_or_get(
|
|
1372
|
-
task_id=task_id,
|
|
1373
|
-
context_id=context_id,
|
|
1374
|
-
kind="tool",
|
|
1375
|
-
name=call_name,
|
|
1376
|
-
args=call_args,
|
|
1377
|
-
)
|
|
1378
|
-
|
|
1379
|
-
def _detect_tool_completion(
|
|
1380
|
-
self, metadata: dict, content: str
|
|
1381
|
-
) -> tuple[bool, str | None, Any]:
|
|
1382
|
-
"""Detect if a tool has completed and return completion info."""
|
|
1383
|
-
tool_info = metadata.get("tool_info", {}) if isinstance(metadata, dict) else {}
|
|
1384
|
-
|
|
1385
|
-
if tool_info.get("status") == "finished" and tool_info.get("name"):
|
|
1386
|
-
return True, tool_info.get("name"), tool_info.get("output")
|
|
1387
|
-
elif content and isinstance(content, str) and content.startswith("Completed "):
|
|
1388
|
-
# content like "Completed google_serper"
|
|
1389
|
-
tname = content.replace("Completed ", "").strip()
|
|
1390
|
-
if tname:
|
|
1391
|
-
output = (
|
|
1392
|
-
tool_info.get("output") if tool_info.get("name") == tname else None
|
|
1393
|
-
)
|
|
1394
|
-
return True, tname, output
|
|
1395
|
-
elif metadata.get("status") == "finished" and tool_info.get("name"):
|
|
1396
|
-
return True, tool_info.get("name"), tool_info.get("output")
|
|
1397
|
-
|
|
1398
|
-
return False, None, None
|
|
1399
|
-
|
|
1400
|
-
def _get_tool_session_id(
|
|
1401
|
-
self, finished_tool_name: str, task_id: str, context_id: str
|
|
1402
|
-
) -> str:
|
|
1403
|
-
"""Generate tool session ID."""
|
|
1404
|
-
return f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
1405
|
-
|
|
1406
|
-
def _calculate_tool_duration(self, meta: dict[str, Any]) -> float | None:
|
|
1407
|
-
"""Calculate tool duration from metadata."""
|
|
1408
|
-
server_now = self.stream_processor.server_elapsed_time
|
|
1409
|
-
server_start = meta.get("server_started_at")
|
|
1410
|
-
dur = None
|
|
1411
|
-
|
|
1412
|
-
try:
|
|
1413
|
-
if isinstance(server_now, (int, float)) and server_start is not None:
|
|
1414
|
-
dur = max(0.0, float(server_now) - float(server_start))
|
|
1415
|
-
else:
|
|
1416
|
-
started_at = meta.get("started_at")
|
|
1417
|
-
if started_at is not None:
|
|
1418
|
-
started_at_float = float(started_at)
|
|
1419
|
-
dur = max(0.0, float(monotonic()) - started_at_float)
|
|
1420
|
-
except (TypeError, ValueError):
|
|
1421
|
-
logger.exception("Failed to calculate tool duration")
|
|
1422
|
-
return None
|
|
1423
|
-
|
|
1424
|
-
return dur
|
|
1425
|
-
|
|
1426
|
-
def _update_tool_metadata(self, meta: dict[str, Any], dur: float | None) -> None:
|
|
1427
|
-
"""Update tool metadata with duration information."""
|
|
1428
|
-
if dur is not None:
|
|
1429
|
-
meta["duration_seconds"] = dur
|
|
1430
|
-
meta["server_finished_at"] = (
|
|
1431
|
-
self.stream_processor.server_elapsed_time
|
|
1432
|
-
if isinstance(self.stream_processor.server_elapsed_time, int | float)
|
|
1433
|
-
else None
|
|
1434
|
-
)
|
|
1435
|
-
meta["finished_at"] = monotonic()
|
|
1436
|
-
|
|
1437
|
-
def _add_tool_output_to_panel(
|
|
1438
|
-
self, meta: dict[str, Any], finished_tool_output: Any, finished_tool_name: str
|
|
1439
|
-
) -> None:
|
|
1440
|
-
"""Add tool output to panel metadata."""
|
|
1441
|
-
if finished_tool_output is not None:
|
|
1442
|
-
meta["chunks"].append(
|
|
1443
|
-
self._format_output_block(finished_tool_output, finished_tool_name)
|
|
1444
|
-
)
|
|
1445
|
-
meta["output"] = finished_tool_output
|
|
1446
|
-
|
|
1447
|
-
def _mark_panel_as_finished(self, meta: dict[str, Any], tool_sid: str) -> None:
|
|
1448
|
-
"""Mark panel as finished and ensure visibility."""
|
|
1449
|
-
if meta.get("status") != "finished":
|
|
1450
|
-
meta["status"] = "finished"
|
|
1451
|
-
|
|
1452
|
-
dur = self._calculate_tool_duration(meta)
|
|
1453
|
-
self._update_tool_metadata(meta, dur)
|
|
1454
|
-
|
|
1455
|
-
# Ensure this finished panel is visible in this frame
|
|
1456
|
-
self.stream_processor.current_event_finished_panels.add(tool_sid)
|
|
1457
|
-
|
|
1458
|
-
def _finish_tool_panel(
|
|
1459
|
-
self,
|
|
1460
|
-
finished_tool_name: str,
|
|
1461
|
-
finished_tool_output: Any,
|
|
1462
|
-
task_id: str,
|
|
1463
|
-
context_id: str,
|
|
1464
|
-
) -> None:
|
|
1465
|
-
"""Finish a tool panel and update its status."""
|
|
1466
|
-
tool_sid = self._get_tool_session_id(finished_tool_name, task_id, context_id)
|
|
1467
|
-
if tool_sid not in self.tool_panels:
|
|
1468
|
-
return
|
|
1469
|
-
|
|
1470
|
-
meta = self.tool_panels[tool_sid]
|
|
1471
|
-
self._mark_panel_as_finished(meta, tool_sid)
|
|
1472
|
-
self._add_tool_output_to_panel(meta, finished_tool_output, finished_tool_name)
|
|
1473
|
-
|
|
1474
|
-
def _get_step_duration(
|
|
1475
|
-
self, finished_tool_name: str, task_id: str, context_id: str
|
|
1476
|
-
) -> float | None:
|
|
1477
|
-
"""Get step duration from tool panels."""
|
|
1478
|
-
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
1479
|
-
return self.tool_panels.get(tool_sid, {}).get("duration_seconds")
|
|
1480
|
-
|
|
1481
|
-
def _finish_delegation_step(
|
|
1482
|
-
self,
|
|
1483
|
-
finished_tool_name: str,
|
|
1484
|
-
finished_tool_output: Any,
|
|
1485
|
-
task_id: str,
|
|
1486
|
-
context_id: str,
|
|
1487
|
-
step_duration: float | None,
|
|
1488
|
-
) -> None:
|
|
1489
|
-
"""Finish a delegation step."""
|
|
1490
|
-
self.steps.finish(
|
|
1491
|
-
task_id=task_id,
|
|
1492
|
-
context_id=context_id,
|
|
1493
|
-
kind="delegate",
|
|
1494
|
-
name=finished_tool_name,
|
|
1495
|
-
output=finished_tool_output,
|
|
1496
|
-
duration_raw=step_duration,
|
|
1497
|
-
)
|
|
1498
|
-
|
|
1499
|
-
def _finish_tool_step_type(
|
|
1500
|
-
self,
|
|
1501
|
-
finished_tool_name: str,
|
|
1502
|
-
finished_tool_output: Any,
|
|
1503
|
-
task_id: str,
|
|
1504
|
-
context_id: str,
|
|
1505
|
-
step_duration: float | None,
|
|
1506
|
-
) -> None:
|
|
1507
|
-
"""Finish a regular tool step."""
|
|
1508
|
-
self.steps.finish(
|
|
1509
|
-
task_id=task_id,
|
|
1510
|
-
context_id=context_id,
|
|
1511
|
-
kind="tool",
|
|
1512
|
-
name=finished_tool_name,
|
|
1513
|
-
output=finished_tool_output,
|
|
1514
|
-
duration_raw=step_duration,
|
|
1515
|
-
)
|
|
1516
|
-
|
|
1517
|
-
def _finish_tool_step(
|
|
1518
|
-
self,
|
|
1519
|
-
finished_tool_name: str,
|
|
1520
|
-
finished_tool_output: Any,
|
|
1521
|
-
task_id: str,
|
|
1522
|
-
context_id: str,
|
|
1523
|
-
*,
|
|
1524
|
-
tracked_step: Step | None = None,
|
|
1525
|
-
) -> None:
|
|
1526
|
-
"""Finish the corresponding step for a completed tool."""
|
|
1527
|
-
if tracked_step is not None:
|
|
1528
|
-
return
|
|
1529
|
-
|
|
1530
|
-
step_duration = self._get_step_duration(finished_tool_name, task_id, context_id)
|
|
1531
|
-
|
|
1532
|
-
if is_delegation_tool(finished_tool_name):
|
|
1533
|
-
self._finish_delegation_step(
|
|
1534
|
-
finished_tool_name,
|
|
1535
|
-
finished_tool_output,
|
|
1536
|
-
task_id,
|
|
1537
|
-
context_id,
|
|
1538
|
-
step_duration,
|
|
1539
|
-
)
|
|
1540
|
-
else:
|
|
1541
|
-
self._finish_tool_step_type(
|
|
1542
|
-
finished_tool_name,
|
|
1543
|
-
finished_tool_output,
|
|
1544
|
-
task_id,
|
|
1545
|
-
context_id,
|
|
1546
|
-
step_duration,
|
|
1547
|
-
)
|
|
1548
|
-
|
|
1549
|
-
def _should_create_snapshot(self, tool_sid: str) -> bool:
|
|
1550
|
-
"""Check if a snapshot should be created."""
|
|
1551
|
-
return self.cfg.append_finished_snapshots and not self.tool_panels.get(
|
|
1552
|
-
tool_sid, {}
|
|
1553
|
-
).get("snapshot_printed")
|
|
1554
|
-
|
|
1555
|
-
def _get_snapshot_title(self, meta: dict[str, Any], finished_tool_name: str) -> str:
|
|
1556
|
-
"""Get the title for the snapshot."""
|
|
1557
|
-
adjusted_title = meta.get("title") or finished_tool_name
|
|
1558
|
-
|
|
1559
|
-
# Add elapsed time to title
|
|
1560
|
-
dur = meta.get("duration_seconds")
|
|
1561
|
-
if isinstance(dur, int | float):
|
|
1562
|
-
elapsed_str = self._format_snapshot_duration(dur)
|
|
1563
|
-
adjusted_title = f"{adjusted_title} · {elapsed_str}"
|
|
1564
|
-
|
|
1565
|
-
return adjusted_title
|
|
1566
|
-
|
|
1567
|
-
def _format_snapshot_duration(self, dur: int | float) -> str:
|
|
1568
|
-
"""Format duration for snapshot title."""
|
|
1569
|
-
try:
|
|
1570
|
-
# Handle invalid types
|
|
1571
|
-
if not isinstance(dur, (int, float)):
|
|
1572
|
-
return "<1ms"
|
|
1573
|
-
|
|
1574
|
-
if dur >= 1:
|
|
1575
|
-
return f"{dur:.2f}s"
|
|
1576
|
-
elif int(dur * 1000) > 0:
|
|
1577
|
-
return f"{int(dur * 1000)}ms"
|
|
1578
|
-
else:
|
|
1579
|
-
return "<1ms"
|
|
1580
|
-
except (TypeError, ValueError, OverflowError):
|
|
1581
|
-
return "<1ms"
|
|
1582
|
-
|
|
1583
|
-
def _clamp_snapshot_body(self, body_text: str) -> str:
|
|
1584
|
-
"""Clamp snapshot body to configured limits."""
|
|
1585
|
-
max_lines = int(self.cfg.snapshot_max_lines or 0)
|
|
1586
|
-
lines = body_text.splitlines()
|
|
1587
|
-
if max_lines > 0 and len(lines) > max_lines:
|
|
1588
|
-
lines = lines[:max_lines] + ["… (truncated)"]
|
|
1589
|
-
body_text = "\n".join(lines)
|
|
1590
|
-
|
|
1591
|
-
max_chars = int(self.cfg.snapshot_max_chars or 0)
|
|
1592
|
-
if max_chars > 0 and len(body_text) > max_chars:
|
|
1593
|
-
suffix = "\n… (truncated)"
|
|
1594
|
-
body_text = body_text[: max_chars - len(suffix)] + suffix
|
|
1595
|
-
|
|
1596
|
-
return body_text
|
|
1597
|
-
|
|
1598
|
-
def _create_snapshot_panel(
|
|
1599
|
-
self, adjusted_title: str, body_text: str, finished_tool_name: str
|
|
1600
|
-
) -> Any:
|
|
1601
|
-
"""Create the snapshot panel."""
|
|
1602
|
-
return create_tool_panel(
|
|
1603
|
-
title=adjusted_title,
|
|
1604
|
-
content=body_text or "(no output)",
|
|
1605
|
-
status="finished",
|
|
1606
|
-
theme=self.cfg.theme,
|
|
1607
|
-
is_delegation=is_delegation_tool(finished_tool_name),
|
|
1608
|
-
)
|
|
1609
|
-
|
|
1610
|
-
def _print_and_mark_snapshot(self, tool_sid: str, snapshot_panel: Any) -> None:
|
|
1611
|
-
"""Print snapshot and mark as printed."""
|
|
1612
|
-
self.console.print(snapshot_panel)
|
|
1613
|
-
self.tool_panels[tool_sid]["snapshot_printed"] = True
|
|
1614
|
-
|
|
1615
|
-
def _create_tool_snapshot(
|
|
1616
|
-
self, finished_tool_name: str, task_id: str, context_id: str
|
|
1617
|
-
) -> None:
|
|
1618
|
-
"""Create and print a snapshot for a finished tool."""
|
|
1619
|
-
tool_sid = f"tool_{finished_tool_name}_{task_id}_{context_id}"
|
|
1620
|
-
|
|
1621
|
-
if not self._should_create_snapshot(tool_sid):
|
|
1622
|
-
return
|
|
1623
|
-
|
|
1624
|
-
meta = self.tool_panels[tool_sid]
|
|
1625
|
-
adjusted_title = self._get_snapshot_title(meta, finished_tool_name)
|
|
1626
|
-
|
|
1627
|
-
# Compose body from chunks and clamp
|
|
1628
|
-
body_text = "".join(meta.get("chunks") or [])
|
|
1629
|
-
body_text = self._clamp_snapshot_body(body_text)
|
|
1630
|
-
|
|
1631
|
-
snapshot_panel = self._create_snapshot_panel(
|
|
1632
|
-
adjusted_title, body_text, finished_tool_name
|
|
1633
|
-
)
|
|
1634
|
-
|
|
1635
|
-
self._print_and_mark_snapshot(tool_sid, snapshot_panel)
|
|
1636
|
-
|
|
1637
|
-
def _handle_agent_step(
|
|
1638
|
-
self,
|
|
1639
|
-
event: dict[str, Any],
|
|
1640
|
-
tool_name: str | None,
|
|
1641
|
-
tool_args: Any,
|
|
1642
|
-
_tool_out: Any,
|
|
1643
|
-
tool_calls_info: list[tuple[str, Any, Any]],
|
|
1644
|
-
*,
|
|
1645
|
-
tracked_step: Step | None = None,
|
|
1646
|
-
) -> None:
|
|
1647
|
-
"""Handle agent step event."""
|
|
1648
|
-
metadata = event.get("metadata", {})
|
|
1649
|
-
task_id = event.get("task_id") or metadata.get("task_id")
|
|
1650
|
-
context_id = event.get("context_id") or metadata.get("context_id")
|
|
1651
|
-
content = event.get("content", "")
|
|
1652
|
-
|
|
1653
|
-
# Create steps and panels for the primary tool
|
|
1654
|
-
if tool_name:
|
|
1655
|
-
tool_sid = self._ensure_tool_panel(
|
|
1656
|
-
tool_name, tool_args, task_id, context_id
|
|
1657
|
-
)
|
|
1658
|
-
self._start_tool_step(
|
|
1659
|
-
task_id,
|
|
1660
|
-
context_id,
|
|
1661
|
-
tool_name,
|
|
1662
|
-
tool_args,
|
|
1663
|
-
tool_sid,
|
|
1664
|
-
tracked_step=tracked_step,
|
|
1665
|
-
)
|
|
1666
|
-
|
|
1667
|
-
# Handle additional tool calls
|
|
1668
|
-
self._process_additional_tool_calls(
|
|
1669
|
-
tool_calls_info, tool_name, task_id, context_id
|
|
1670
|
-
)
|
|
1671
|
-
|
|
1672
|
-
# Check for tool completion
|
|
1673
|
-
(
|
|
1674
|
-
is_tool_finished,
|
|
1675
|
-
finished_tool_name,
|
|
1676
|
-
finished_tool_output,
|
|
1677
|
-
) = self._detect_tool_completion(metadata, content)
|
|
1678
|
-
|
|
1679
|
-
if is_tool_finished and finished_tool_name:
|
|
1680
|
-
self._finish_tool_panel(
|
|
1681
|
-
finished_tool_name, finished_tool_output, task_id, context_id
|
|
1682
|
-
)
|
|
1683
|
-
self._finish_tool_step(
|
|
1684
|
-
finished_tool_name,
|
|
1685
|
-
finished_tool_output,
|
|
1686
|
-
task_id,
|
|
1687
|
-
context_id,
|
|
1688
|
-
tracked_step=tracked_step,
|
|
1689
|
-
)
|
|
1690
|
-
self._create_tool_snapshot(finished_tool_name, task_id, context_id)
|
|
1691
|
-
|
|
1692
|
-
def _spinner(self) -> str:
|
|
1693
|
-
"""Return spinner character."""
|
|
1694
|
-
return get_spinner()
|
|
1695
|
-
|
|
1696
754
|
def _format_working_indicator(self, started_at: float | None) -> str:
|
|
1697
755
|
"""Format working indicator."""
|
|
1698
756
|
return format_working_indicator(
|
|
@@ -1735,9 +793,7 @@ class RichStreamRenderer:
|
|
|
1735
793
|
|
|
1736
794
|
def _get_analysis_progress_info(self) -> dict[str, Any]:
|
|
1737
795
|
total_steps = len(self.steps.order)
|
|
1738
|
-
completed_steps = sum(
|
|
1739
|
-
1 for sid in self.steps.order if is_step_finished(self.steps.by_id[sid])
|
|
1740
|
-
)
|
|
796
|
+
completed_steps = sum(1 for sid in self.steps.order if is_step_finished(self.steps.by_id[sid]))
|
|
1741
797
|
current_step = None
|
|
1742
798
|
for sid in self.steps.order:
|
|
1743
799
|
if not is_step_finished(self.steps.by_id[sid]):
|
|
@@ -1745,13 +801,11 @@ class RichStreamRenderer:
|
|
|
1745
801
|
break
|
|
1746
802
|
# Prefer server elapsed time when available
|
|
1747
803
|
elapsed = 0.0
|
|
1748
|
-
if isinstance(self.stream_processor.server_elapsed_time, int
|
|
804
|
+
if isinstance(self.stream_processor.server_elapsed_time, (int, float)):
|
|
1749
805
|
elapsed = float(self.stream_processor.server_elapsed_time)
|
|
1750
806
|
elif self._started_at is not None:
|
|
1751
807
|
elapsed = monotonic() - self._started_at
|
|
1752
|
-
progress_percent = (
|
|
1753
|
-
int((completed_steps / total_steps) * 100) if total_steps else 0
|
|
1754
|
-
)
|
|
808
|
+
progress_percent = int((completed_steps / total_steps) * 100) if total_steps else 0
|
|
1755
809
|
return {
|
|
1756
810
|
"total_steps": total_steps,
|
|
1757
811
|
"completed_steps": completed_steps,
|
|
@@ -1840,9 +894,7 @@ class RichStreamRenderer:
|
|
|
1840
894
|
server_elapsed = self.stream_processor.server_elapsed_time
|
|
1841
895
|
server_start = self._step_server_start_times.get(step.step_id)
|
|
1842
896
|
|
|
1843
|
-
if isinstance(server_elapsed, int
|
|
1844
|
-
server_start, int | float
|
|
1845
|
-
):
|
|
897
|
+
if isinstance(server_elapsed, (int, float)) and isinstance(server_start, (int, float)):
|
|
1846
898
|
return max(0.0, float(server_elapsed) - float(server_start))
|
|
1847
899
|
|
|
1848
900
|
try:
|
|
@@ -1858,18 +910,7 @@ class RichStreamRenderer:
|
|
|
1858
910
|
|
|
1859
911
|
def _resolve_step_label(self, step: Step) -> str:
|
|
1860
912
|
"""Return the display label for a step with sensible fallbacks."""
|
|
1861
|
-
|
|
1862
|
-
label = raw_label.strip() if isinstance(raw_label, str) else ""
|
|
1863
|
-
if label:
|
|
1864
|
-
return normalise_display_label(label)
|
|
1865
|
-
|
|
1866
|
-
if not (step.name or "").strip():
|
|
1867
|
-
return "Unknown step detail"
|
|
1868
|
-
|
|
1869
|
-
icon = self._get_step_icon(step.kind)
|
|
1870
|
-
base_name = self._get_step_display_name(step)
|
|
1871
|
-
fallback = " ".join(part for part in (icon, base_name) if part).strip()
|
|
1872
|
-
return normalise_display_label(fallback)
|
|
913
|
+
return format_step_label(step)
|
|
1873
914
|
|
|
1874
915
|
def _check_parallel_tools(self) -> dict[tuple[str | None, str | None], list]:
|
|
1875
916
|
"""Check for parallel running tools."""
|
|
@@ -1890,358 +931,77 @@ class RichStreamRenderer:
|
|
|
1890
931
|
key = (step.task_id, step.context_id)
|
|
1891
932
|
return len(running_by_ctx.get(key, [])) > 1
|
|
1892
933
|
|
|
1893
|
-
def
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
"""Compose a single renderable for the hierarchical steps panel."""
|
|
1899
|
-
prefix = build_connector_prefix(branch_state)
|
|
1900
|
-
text_line = self._build_step_text_line(step, prefix)
|
|
1901
|
-
renderables = self._wrap_step_text(step, text_line)
|
|
1902
|
-
|
|
1903
|
-
args_renderable = self._build_args_renderable(step, prefix)
|
|
1904
|
-
if args_renderable is not None:
|
|
1905
|
-
renderables.append(args_renderable)
|
|
1906
|
-
|
|
1907
|
-
return self._collapse_renderables(renderables)
|
|
1908
|
-
|
|
1909
|
-
def _build_step_text_line(
|
|
1910
|
-
self,
|
|
1911
|
-
step: Step,
|
|
1912
|
-
prefix: str,
|
|
1913
|
-
) -> Text:
|
|
1914
|
-
"""Create the textual portion of a step renderable."""
|
|
1915
|
-
text_line = Text()
|
|
1916
|
-
text_line.append(prefix, style="dim")
|
|
1917
|
-
text_line.append(self._resolve_step_label(step))
|
|
1918
|
-
|
|
1919
|
-
status_badge = self._format_step_status(step)
|
|
1920
|
-
self._append_status_badge(text_line, step, status_badge)
|
|
1921
|
-
self._append_state_glyph(text_line, step)
|
|
1922
|
-
return text_line
|
|
1923
|
-
|
|
1924
|
-
def _append_status_badge(
|
|
1925
|
-
self, text_line: Text, step: Step, status_badge: str
|
|
1926
|
-
) -> None:
|
|
1927
|
-
"""Append the formatted status badge when available."""
|
|
1928
|
-
glyph_key = getattr(step, "status_icon", None)
|
|
1929
|
-
glyph = glyph_for_status(glyph_key)
|
|
1930
|
-
|
|
1931
|
-
if status_badge:
|
|
1932
|
-
text_line.append(" ")
|
|
1933
|
-
text_line.append(status_badge, style="cyan")
|
|
1934
|
-
|
|
1935
|
-
if glyph:
|
|
1936
|
-
text_line.append(" ")
|
|
1937
|
-
style = self._status_icon_style(glyph_key)
|
|
1938
|
-
if style:
|
|
1939
|
-
text_line.append(glyph, style=style)
|
|
1940
|
-
else:
|
|
1941
|
-
text_line.append(glyph)
|
|
1942
|
-
|
|
1943
|
-
def _append_state_glyph(self, text_line: Text, step: Step) -> None:
|
|
1944
|
-
"""Append glyph/failure markers in a single place."""
|
|
1945
|
-
failure_reason = (step.failure_reason or "").strip()
|
|
1946
|
-
if failure_reason:
|
|
1947
|
-
text_line.append(f" {failure_reason}")
|
|
1948
|
-
|
|
1949
|
-
@staticmethod
|
|
1950
|
-
def _status_icon_style(icon_key: str | None) -> str | None:
|
|
1951
|
-
"""Return style for a given status icon."""
|
|
1952
|
-
if not icon_key:
|
|
1953
|
-
return None
|
|
1954
|
-
return STATUS_ICON_STYLES.get(icon_key)
|
|
1955
|
-
|
|
1956
|
-
def _wrap_step_text(self, step: Step, text_line: Text) -> list[Any]:
|
|
1957
|
-
"""Return the base text, optionally decorated with a trailing spinner."""
|
|
1958
|
-
if getattr(step, "status", None) == "running":
|
|
1959
|
-
spinner = self._step_spinners.get(step.step_id)
|
|
1960
|
-
if spinner is None:
|
|
1961
|
-
spinner = Spinner("dots", style="dim")
|
|
1962
|
-
self._step_spinners[step.step_id] = spinner
|
|
1963
|
-
return [TrailingSpinnerLine(text_line, spinner)]
|
|
1964
|
-
|
|
1965
|
-
self._step_spinners.pop(step.step_id, None)
|
|
1966
|
-
return [text_line]
|
|
1967
|
-
|
|
1968
|
-
def _collapse_renderables(self, renderables: list[Any]) -> Any:
|
|
1969
|
-
"""Collapse a list of renderables into a single object."""
|
|
1970
|
-
if not renderables:
|
|
1971
|
-
return None
|
|
1972
|
-
|
|
1973
|
-
if len(renderables) == 1:
|
|
1974
|
-
return renderables[0]
|
|
1975
|
-
|
|
1976
|
-
return Group(*renderables)
|
|
1977
|
-
|
|
1978
|
-
def _build_args_renderable(self, step: Step, prefix: str) -> Text | Group | None:
|
|
1979
|
-
"""Build a dimmed argument line for tool or agent steps."""
|
|
1980
|
-
if step.kind not in {"tool", "delegate", "agent"}:
|
|
1981
|
-
return None
|
|
1982
|
-
if step.kind == "agent" and step.parent_id:
|
|
1983
|
-
return None
|
|
1984
|
-
formatted_args = self._format_step_args(step)
|
|
1985
|
-
if not formatted_args:
|
|
1986
|
-
return None
|
|
1987
|
-
if isinstance(formatted_args, list):
|
|
1988
|
-
return self._build_arg_list(prefix, formatted_args)
|
|
1989
|
-
|
|
1990
|
-
args_text = Text()
|
|
1991
|
-
args_text.append(prefix, style="dim")
|
|
1992
|
-
args_text.append(" " * 5)
|
|
1993
|
-
args_text.append(formatted_args, style="dim")
|
|
1994
|
-
return args_text
|
|
1995
|
-
|
|
1996
|
-
def _build_arg_list(
|
|
1997
|
-
self, prefix: str, formatted_args: list[str | tuple[int, str]]
|
|
1998
|
-
) -> Group | None:
|
|
1999
|
-
"""Render multi-line argument entries preserving indentation."""
|
|
2000
|
-
arg_lines: list[Text] = []
|
|
2001
|
-
for indent_level, text_value in self._iter_arg_entries(formatted_args):
|
|
2002
|
-
arg_text = Text()
|
|
2003
|
-
arg_text.append(prefix, style="dim")
|
|
2004
|
-
arg_text.append(" " * 5)
|
|
2005
|
-
arg_text.append(" " * (indent_level * 2))
|
|
2006
|
-
arg_text.append(text_value, style="dim")
|
|
2007
|
-
arg_lines.append(arg_text)
|
|
2008
|
-
if not arg_lines:
|
|
2009
|
-
return None
|
|
2010
|
-
return Group(*arg_lines)
|
|
2011
|
-
|
|
2012
|
-
@staticmethod
|
|
2013
|
-
def _iter_arg_entries(
|
|
2014
|
-
formatted_args: list[str | tuple[int, str]],
|
|
2015
|
-
) -> Iterable[tuple[int, str]]:
|
|
2016
|
-
"""Yield normalized indentation/value pairs for argument entries."""
|
|
2017
|
-
for value in formatted_args:
|
|
2018
|
-
if isinstance(value, tuple) and len(value) == 2:
|
|
2019
|
-
indent_level, text_value = value
|
|
2020
|
-
yield indent_level, str(text_value)
|
|
2021
|
-
else:
|
|
2022
|
-
yield 0, str(value)
|
|
2023
|
-
|
|
2024
|
-
def _format_step_args(
|
|
2025
|
-
self, step: Step
|
|
2026
|
-
) -> str | list[str] | list[tuple[int, str]] | None:
|
|
2027
|
-
"""Return a printable representation of tool arguments."""
|
|
2028
|
-
args = getattr(step, "args", None)
|
|
2029
|
-
if args is None:
|
|
2030
|
-
return None
|
|
2031
|
-
|
|
2032
|
-
if isinstance(args, dict):
|
|
2033
|
-
return self._format_dict_args(args, step=step)
|
|
2034
|
-
|
|
2035
|
-
if isinstance(args, (list, tuple)):
|
|
2036
|
-
return self._safe_pretty_args(list(args))
|
|
2037
|
-
|
|
2038
|
-
if isinstance(args, (str, int, float)):
|
|
2039
|
-
return self._stringify_args(args)
|
|
2040
|
-
|
|
2041
|
-
return None
|
|
2042
|
-
|
|
2043
|
-
def _format_dict_args(
|
|
2044
|
-
self, args: dict[str, Any], *, step: Step
|
|
2045
|
-
) -> str | list[str] | list[tuple[int, str]] | None:
|
|
2046
|
-
"""Format dictionary arguments with guardrails."""
|
|
2047
|
-
if not args:
|
|
2048
|
-
return None
|
|
2049
|
-
|
|
2050
|
-
masked_args = self._redact_arg_payload(args)
|
|
2051
|
-
|
|
2052
|
-
if self._should_collapse_single_query(step):
|
|
2053
|
-
single_query = self._extract_single_query_arg(masked_args)
|
|
2054
|
-
if single_query:
|
|
2055
|
-
return single_query
|
|
2056
|
-
|
|
2057
|
-
return self._format_dict_arg_lines(masked_args)
|
|
2058
|
-
|
|
2059
|
-
@staticmethod
|
|
2060
|
-
def _extract_single_query_arg(args: dict[str, Any]) -> str | None:
|
|
2061
|
-
"""Return a trimmed query argument when it is the only entry."""
|
|
2062
|
-
if len(args) != 1:
|
|
2063
|
-
return None
|
|
2064
|
-
key, value = next(iter(args.items()))
|
|
2065
|
-
if key != "query" or not isinstance(value, str):
|
|
2066
|
-
return None
|
|
2067
|
-
stripped = value.strip()
|
|
2068
|
-
return stripped or None
|
|
2069
|
-
|
|
2070
|
-
@staticmethod
|
|
2071
|
-
def _redact_arg_payload(args: dict[str, Any]) -> dict[str, Any]:
|
|
2072
|
-
"""Apply best-effort masking before rendering arguments."""
|
|
2073
|
-
try:
|
|
2074
|
-
cleaned = redact_sensitive(args)
|
|
2075
|
-
return cleaned if isinstance(cleaned, dict) else args
|
|
2076
|
-
except Exception:
|
|
2077
|
-
return args
|
|
2078
|
-
|
|
2079
|
-
@staticmethod
|
|
2080
|
-
def _should_collapse_single_query(step: Step) -> bool:
|
|
2081
|
-
"""Return True when we should display raw query text."""
|
|
2082
|
-
if step.kind == "agent":
|
|
2083
|
-
return True
|
|
2084
|
-
if step.kind == "delegate":
|
|
2085
|
-
return True
|
|
2086
|
-
return False
|
|
2087
|
-
|
|
2088
|
-
def _format_dict_arg_lines(
|
|
2089
|
-
self, args: dict[str, Any]
|
|
2090
|
-
) -> list[tuple[int, str]] | None:
|
|
2091
|
-
"""Render dictionary arguments as nested YAML-style lines."""
|
|
2092
|
-
lines: list[tuple[int, str]] = []
|
|
2093
|
-
for raw_key, value in args.items():
|
|
2094
|
-
key = str(raw_key)
|
|
2095
|
-
lines.extend(self._format_nested_entry(key, value, indent=0))
|
|
2096
|
-
return lines or None
|
|
2097
|
-
|
|
2098
|
-
def _format_nested_entry(
|
|
2099
|
-
self, key: str, value: Any, indent: int
|
|
2100
|
-
) -> list[tuple[int, str]]:
|
|
2101
|
-
"""Format a mapping entry recursively."""
|
|
2102
|
-
lines: list[tuple[int, str]] = []
|
|
2103
|
-
|
|
2104
|
-
if isinstance(value, dict):
|
|
2105
|
-
if value:
|
|
2106
|
-
lines.append((indent, f"{key}:"))
|
|
2107
|
-
lines.extend(self._format_nested_mapping(value, indent + 1))
|
|
2108
|
-
else:
|
|
2109
|
-
lines.append((indent, f"{key}: {{}}"))
|
|
2110
|
-
return lines
|
|
2111
|
-
|
|
2112
|
-
if isinstance(value, (list, tuple, set)):
|
|
2113
|
-
seq_lines = self._format_sequence_entries(list(value), indent + 1)
|
|
2114
|
-
if seq_lines:
|
|
2115
|
-
lines.append((indent, f"{key}:"))
|
|
2116
|
-
lines.extend(seq_lines)
|
|
2117
|
-
else:
|
|
2118
|
-
lines.append((indent, f"{key}: []"))
|
|
2119
|
-
return lines
|
|
2120
|
-
|
|
2121
|
-
formatted_value = self._format_arg_value(value)
|
|
2122
|
-
if formatted_value is not None:
|
|
2123
|
-
lines.append((indent, f"{key}: {formatted_value}"))
|
|
2124
|
-
return lines
|
|
2125
|
-
|
|
2126
|
-
def _format_nested_mapping(
|
|
2127
|
-
self, mapping: dict[str, Any], indent: int
|
|
2128
|
-
) -> list[tuple[int, str]]:
|
|
2129
|
-
"""Format nested dictionary values."""
|
|
2130
|
-
nested_lines: list[tuple[int, str]] = []
|
|
2131
|
-
for raw_key, value in mapping.items():
|
|
2132
|
-
key = str(raw_key)
|
|
2133
|
-
nested_lines.extend(self._format_nested_entry(key, value, indent))
|
|
2134
|
-
return nested_lines
|
|
2135
|
-
|
|
2136
|
-
def _format_sequence_entries(
|
|
2137
|
-
self, sequence: list[Any], indent: int
|
|
2138
|
-
) -> list[tuple[int, str]]:
|
|
2139
|
-
"""Format list/tuple/set values with YAML-style bullets."""
|
|
2140
|
-
if not sequence:
|
|
2141
|
-
return []
|
|
2142
|
-
|
|
2143
|
-
lines: list[tuple[int, str]] = []
|
|
2144
|
-
for item in sequence:
|
|
2145
|
-
lines.extend(self._format_sequence_item(item, indent))
|
|
2146
|
-
return lines
|
|
2147
|
-
|
|
2148
|
-
def _format_sequence_item(self, item: Any, indent: int) -> list[tuple[int, str]]:
|
|
2149
|
-
"""Format a single list entry."""
|
|
2150
|
-
if isinstance(item, dict):
|
|
2151
|
-
return self._format_dict_sequence_item(item, indent)
|
|
2152
|
-
|
|
2153
|
-
if isinstance(item, (list, tuple, set)):
|
|
2154
|
-
return self._format_nested_sequence_item(list(item), indent)
|
|
2155
|
-
|
|
2156
|
-
formatted = self._format_arg_value(item)
|
|
2157
|
-
if formatted is not None:
|
|
2158
|
-
return [(indent, f"- {formatted}")]
|
|
2159
|
-
return []
|
|
2160
|
-
|
|
2161
|
-
def _format_dict_sequence_item(
|
|
2162
|
-
self, mapping: dict[str, Any], indent: int
|
|
2163
|
-
) -> list[tuple[int, str]]:
|
|
2164
|
-
"""Format a dictionary entry within a list."""
|
|
2165
|
-
child_lines = self._format_nested_mapping(mapping, indent + 1)
|
|
2166
|
-
if child_lines:
|
|
2167
|
-
return self._prepend_sequence_prefix(child_lines, indent)
|
|
2168
|
-
return [(indent, "- {}")]
|
|
2169
|
-
|
|
2170
|
-
def _format_nested_sequence_item(
|
|
2171
|
-
self, sequence: list[Any], indent: int
|
|
2172
|
-
) -> list[tuple[int, str]]:
|
|
2173
|
-
"""Format a nested sequence entry within a list."""
|
|
2174
|
-
child_lines = self._format_sequence_entries(sequence, indent + 1)
|
|
2175
|
-
if child_lines:
|
|
2176
|
-
return self._prepend_sequence_prefix(child_lines, indent)
|
|
2177
|
-
return [(indent, "- []")]
|
|
2178
|
-
|
|
2179
|
-
@staticmethod
|
|
2180
|
-
def _prepend_sequence_prefix(
|
|
2181
|
-
child_lines: list[tuple[int, str]], indent: int
|
|
2182
|
-
) -> list[tuple[int, str]]:
|
|
2183
|
-
"""Attach a sequence bullet to the first child line."""
|
|
2184
|
-
_, first_text = child_lines[0]
|
|
2185
|
-
prefixed: list[tuple[int, str]] = [(indent, f"- {first_text}")]
|
|
2186
|
-
prefixed.extend(child_lines[1:])
|
|
2187
|
-
return prefixed
|
|
2188
|
-
|
|
2189
|
-
def _format_arg_value(self, value: Any) -> str | None:
|
|
2190
|
-
"""Format a single argument value with per-value truncation."""
|
|
2191
|
-
if value is None:
|
|
2192
|
-
return "null"
|
|
2193
|
-
if isinstance(value, (bool, int, float)):
|
|
2194
|
-
return json.dumps(value, ensure_ascii=False)
|
|
2195
|
-
if isinstance(value, str):
|
|
2196
|
-
return self._format_string_arg_value(value)
|
|
2197
|
-
return _truncate_display(str(value), limit=ARGS_VALUE_MAX_LEN)
|
|
2198
|
-
|
|
2199
|
-
@staticmethod
|
|
2200
|
-
def _format_string_arg_value(value: str) -> str:
|
|
2201
|
-
"""Return a trimmed, quoted representation of a string argument."""
|
|
2202
|
-
sanitised = value.replace("\n", " ").strip()
|
|
2203
|
-
sanitised = sanitised.replace('"', '\\"')
|
|
2204
|
-
trimmed = _truncate_display(sanitised, limit=ARGS_VALUE_MAX_LEN)
|
|
2205
|
-
return f'"{trimmed}"'
|
|
2206
|
-
|
|
2207
|
-
@staticmethod
|
|
2208
|
-
def _safe_pretty_args(args: dict[str, Any]) -> str | None:
|
|
2209
|
-
"""Defensively format argument dictionaries."""
|
|
2210
|
-
try:
|
|
2211
|
-
return pretty_args(args, max_len=160)
|
|
2212
|
-
except Exception:
|
|
2213
|
-
return str(args)
|
|
2214
|
-
|
|
2215
|
-
@staticmethod
|
|
2216
|
-
def _stringify_args(args: Any) -> str | None:
|
|
2217
|
-
"""Format non-dictionary argument payloads."""
|
|
2218
|
-
text = str(args).strip()
|
|
2219
|
-
if not text:
|
|
2220
|
-
return None
|
|
2221
|
-
return _truncate_display(text)
|
|
2222
|
-
|
|
2223
|
-
def _render_steps_text(self) -> Any:
|
|
2224
|
-
"""Render the steps panel content."""
|
|
2225
|
-
if not (self.steps.order or self.steps.children):
|
|
2226
|
-
return Text("No steps yet", style="dim")
|
|
2227
|
-
|
|
2228
|
-
renderables: list[Any] = []
|
|
2229
|
-
for step_id, branch_state in self.steps.iter_tree():
|
|
2230
|
-
step = self.steps.by_id.get(step_id)
|
|
934
|
+
def _build_step_status_overrides(self) -> dict[str, str]:
|
|
935
|
+
"""Return status text overrides for steps (running duration badges)."""
|
|
936
|
+
overrides: dict[str, str] = {}
|
|
937
|
+
for sid in self.steps.order:
|
|
938
|
+
step = self.steps.by_id.get(sid)
|
|
2231
939
|
if not step:
|
|
2232
940
|
continue
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
941
|
+
try:
|
|
942
|
+
status_text = self._format_step_status(step)
|
|
943
|
+
except Exception:
|
|
944
|
+
status_text = ""
|
|
945
|
+
if status_text:
|
|
946
|
+
overrides[sid] = status_text
|
|
947
|
+
return overrides
|
|
948
|
+
|
|
949
|
+
def _resolve_steps_panel(self) -> AIPPanel:
|
|
950
|
+
"""Return the shared steps panel renderable generated by layout helpers."""
|
|
951
|
+
window_arg = self._summary_window_size()
|
|
952
|
+
window_arg = window_arg if window_arg > 0 else None
|
|
953
|
+
panels = render_summary_panels(
|
|
954
|
+
self.state,
|
|
955
|
+
self.steps,
|
|
956
|
+
summary_window=window_arg,
|
|
957
|
+
include_query_panel=False,
|
|
958
|
+
include_final_panel=False,
|
|
959
|
+
step_status_overrides=self._build_step_status_overrides(),
|
|
960
|
+
)
|
|
961
|
+
steps_panel = next((panel for panel in panels if getattr(panel, "title", "").lower() == "steps"), None)
|
|
962
|
+
panel_cls = AIPPanel if isinstance(AIPPanel, type) else None
|
|
963
|
+
if steps_panel is not None and (panel_cls is None or isinstance(steps_panel, panel_cls)):
|
|
964
|
+
return steps_panel
|
|
965
|
+
return AIPPanel(_NO_STEPS_TEXT.copy(), title="Steps", border_style="blue")
|
|
966
|
+
|
|
967
|
+
def _prepare_steps_renderable(self, *, include_progress: bool) -> tuple[AIPPanel, Any]:
|
|
968
|
+
"""Return the template panel and content renderable for steps."""
|
|
969
|
+
panel = self._resolve_steps_panel()
|
|
970
|
+
self._last_steps_panel_template = panel
|
|
971
|
+
base_renderable: Any = getattr(panel, "renderable", panel)
|
|
972
|
+
|
|
973
|
+
if include_progress and not self.state.finalizing_ui:
|
|
974
|
+
footer = build_progress_footer(
|
|
975
|
+
state=self.state,
|
|
976
|
+
steps=self.steps,
|
|
977
|
+
started_at=self._started_at,
|
|
978
|
+
server_elapsed_time=self.stream_processor.server_elapsed_time,
|
|
979
|
+
)
|
|
980
|
+
if footer is not None:
|
|
981
|
+
if isinstance(base_renderable, Group):
|
|
982
|
+
base_renderable = Group(*base_renderable.renderables, footer)
|
|
983
|
+
else:
|
|
984
|
+
base_renderable = Group(base_renderable, footer)
|
|
985
|
+
return panel, base_renderable
|
|
986
|
+
|
|
987
|
+
def _build_steps_body(self, *, include_progress: bool) -> Any:
|
|
988
|
+
"""Return the rendered steps body with optional progress footer."""
|
|
989
|
+
_, renderable = self._prepare_steps_renderable(include_progress=include_progress)
|
|
990
|
+
if isinstance(renderable, Group):
|
|
991
|
+
return renderable
|
|
992
|
+
return Group(renderable)
|
|
2236
993
|
|
|
2237
|
-
|
|
2238
|
-
|
|
994
|
+
def _render_steps_text(self) -> Any:
|
|
995
|
+
"""Return the rendered steps body used by transcript capture."""
|
|
996
|
+
return self._build_steps_body(include_progress=True)
|
|
2239
997
|
|
|
2240
|
-
|
|
998
|
+
def _summary_window_size(self) -> int:
|
|
999
|
+
"""Return the active window size for step display."""
|
|
1000
|
+
if self.state.finalizing_ui:
|
|
1001
|
+
return 0
|
|
1002
|
+
return int(self.cfg.summary_display_window or 0)
|
|
2241
1003
|
|
|
2242
|
-
def _update_final_duration(
|
|
2243
|
-
self, duration: float | None, *, overwrite: bool = False
|
|
2244
|
-
) -> None:
|
|
1004
|
+
def _update_final_duration(self, duration: float | None, *, overwrite: bool = False) -> None:
|
|
2245
1005
|
"""Store formatted duration for eventual final panels."""
|
|
2246
1006
|
if duration is None:
|
|
2247
1007
|
return
|
|
@@ -2259,85 +1019,6 @@ class RichStreamRenderer:
|
|
|
2259
1019
|
if overwrite and existing is not None:
|
|
2260
1020
|
duration_val = max(existing, duration_val)
|
|
2261
1021
|
|
|
2262
|
-
|
|
2263
|
-
self.state.
|
|
1022
|
+
formatted = format_elapsed_time(duration_val)
|
|
1023
|
+
self.state.mark_final_duration(duration_val, formatted=formatted)
|
|
2264
1024
|
self._apply_root_duration(duration_val)
|
|
2265
|
-
|
|
2266
|
-
def _format_elapsed_time(self, elapsed: float) -> str:
|
|
2267
|
-
"""Format elapsed time as a readable string."""
|
|
2268
|
-
if elapsed >= 1:
|
|
2269
|
-
return f"{elapsed:.2f}s"
|
|
2270
|
-
elif int(elapsed * 1000) > 0:
|
|
2271
|
-
return f"{int(elapsed * 1000)}ms"
|
|
2272
|
-
else:
|
|
2273
|
-
return "<1ms"
|
|
2274
|
-
|
|
2275
|
-
def _format_dict_or_list_output(self, output_value: dict | list) -> str:
|
|
2276
|
-
"""Format dict/list output as pretty JSON."""
|
|
2277
|
-
try:
|
|
2278
|
-
return (
|
|
2279
|
-
self.OUTPUT_PREFIX
|
|
2280
|
-
+ "```json\n"
|
|
2281
|
-
+ json.dumps(output_value, indent=2)
|
|
2282
|
-
+ "\n```\n"
|
|
2283
|
-
)
|
|
2284
|
-
except Exception:
|
|
2285
|
-
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
2286
|
-
|
|
2287
|
-
def _clean_sub_agent_prefix(self, output: str, tool_name: str | None) -> str:
|
|
2288
|
-
"""Clean sub-agent name prefix from output."""
|
|
2289
|
-
if not (tool_name and is_delegation_tool(tool_name)):
|
|
2290
|
-
return output
|
|
2291
|
-
|
|
2292
|
-
sub = tool_name
|
|
2293
|
-
if tool_name.startswith("delegate_to_"):
|
|
2294
|
-
sub = tool_name.replace("delegate_to_", "")
|
|
2295
|
-
elif tool_name.startswith("delegate_"):
|
|
2296
|
-
sub = tool_name.replace("delegate_", "")
|
|
2297
|
-
prefix = f"[{sub}]"
|
|
2298
|
-
if output.startswith(prefix):
|
|
2299
|
-
return output[len(prefix) :].lstrip()
|
|
2300
|
-
|
|
2301
|
-
return output
|
|
2302
|
-
|
|
2303
|
-
def _format_json_string_output(self, output: str) -> str:
|
|
2304
|
-
"""Format string that looks like JSON."""
|
|
2305
|
-
try:
|
|
2306
|
-
parsed = json.loads(output)
|
|
2307
|
-
return (
|
|
2308
|
-
self.OUTPUT_PREFIX
|
|
2309
|
-
+ "```json\n"
|
|
2310
|
-
+ json.dumps(parsed, indent=2)
|
|
2311
|
-
+ "\n```\n"
|
|
2312
|
-
)
|
|
2313
|
-
except Exception:
|
|
2314
|
-
return self.OUTPUT_PREFIX + output + "\n"
|
|
2315
|
-
|
|
2316
|
-
def _format_string_output(self, output: str, tool_name: str | None) -> str:
|
|
2317
|
-
"""Format string output with optional prefix cleaning."""
|
|
2318
|
-
s = output.strip()
|
|
2319
|
-
s = self._clean_sub_agent_prefix(s, tool_name)
|
|
2320
|
-
|
|
2321
|
-
# If looks like JSON, pretty print it
|
|
2322
|
-
if (s.startswith("{") and s.endswith("}")) or (
|
|
2323
|
-
s.startswith("[") and s.endswith("]")
|
|
2324
|
-
):
|
|
2325
|
-
return self._format_json_string_output(s)
|
|
2326
|
-
|
|
2327
|
-
return self.OUTPUT_PREFIX + s + "\n"
|
|
2328
|
-
|
|
2329
|
-
def _format_other_output(self, output_value: Any) -> str:
|
|
2330
|
-
"""Format other types of output."""
|
|
2331
|
-
try:
|
|
2332
|
-
return self.OUTPUT_PREFIX + json.dumps(output_value, indent=2) + "\n"
|
|
2333
|
-
except Exception:
|
|
2334
|
-
return self.OUTPUT_PREFIX + str(output_value) + "\n"
|
|
2335
|
-
|
|
2336
|
-
def _format_output_block(self, output_value: Any, tool_name: str | None) -> str:
|
|
2337
|
-
"""Format an output value for panel display."""
|
|
2338
|
-
if isinstance(output_value, dict | list):
|
|
2339
|
-
return self._format_dict_or_list_output(output_value)
|
|
2340
|
-
elif isinstance(output_value, str):
|
|
2341
|
-
return self._format_string_output(output_value, tool_name)
|
|
2342
|
-
else:
|
|
2343
|
-
return self._format_other_output(output_value)
|