glaip-sdk 0.0.20__py3-none-any.whl → 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- glaip_sdk/cli/commands/agents.py +19 -0
- glaip_sdk/cli/commands/mcps.py +1 -2
- glaip_sdk/cli/slash/session.py +0 -3
- glaip_sdk/cli/transcript/viewer.py +176 -6
- glaip_sdk/cli/utils.py +0 -1
- glaip_sdk/client/run_rendering.py +125 -20
- glaip_sdk/icons.py +9 -3
- glaip_sdk/utils/rendering/formatting.py +50 -7
- glaip_sdk/utils/rendering/models.py +15 -2
- glaip_sdk/utils/rendering/renderer/__init__.py +0 -2
- glaip_sdk/utils/rendering/renderer/base.py +1131 -218
- glaip_sdk/utils/rendering/renderer/config.py +3 -5
- glaip_sdk/utils/rendering/renderer/stream.py +3 -3
- glaip_sdk/utils/rendering/renderer/toggle.py +184 -0
- glaip_sdk/utils/rendering/step_tree_state.py +102 -0
- glaip_sdk/utils/rendering/steps.py +944 -16
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/METADATA +12 -1
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/RECORD +20 -18
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/WHEEL +0 -0
- {glaip_sdk-0.0.20.dist-info → glaip_sdk-0.1.0.dist-info}/entry_points.txt +0 -0
glaip_sdk/cli/commands/agents.py
CHANGED
|
@@ -83,6 +83,7 @@ from glaip_sdk.icons import ICON_AGENT
|
|
|
83
83
|
from glaip_sdk.utils import format_datetime, is_uuid
|
|
84
84
|
from glaip_sdk.utils.agent_config import normalize_agent_config_for_import
|
|
85
85
|
from glaip_sdk.utils.import_export import convert_export_to_import_format
|
|
86
|
+
from glaip_sdk.utils.rendering.renderer.toggle import TranscriptToggleController
|
|
86
87
|
from glaip_sdk.utils.validation import coerce_timeout
|
|
87
88
|
|
|
88
89
|
console = Console()
|
|
@@ -598,6 +599,23 @@ def _setup_run_renderer(ctx: Any, save: str | None, verbose: bool) -> Any:
|
|
|
598
599
|
)
|
|
599
600
|
|
|
600
601
|
|
|
602
|
+
def _maybe_attach_transcript_toggle(ctx: Any, renderer: Any) -> None:
|
|
603
|
+
"""Attach transcript toggle controller when interactive TTY is available."""
|
|
604
|
+
if renderer is None:
|
|
605
|
+
return
|
|
606
|
+
|
|
607
|
+
console_obj = getattr(renderer, "console", None)
|
|
608
|
+
if console_obj is None or not getattr(console_obj, "is_terminal", False):
|
|
609
|
+
return
|
|
610
|
+
|
|
611
|
+
tty_enabled = bool(get_ctx_value(ctx, "tty", True))
|
|
612
|
+
if not tty_enabled:
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
controller = TranscriptToggleController(enabled=True)
|
|
616
|
+
renderer.transcript_controller = controller
|
|
617
|
+
|
|
618
|
+
|
|
601
619
|
def _prepare_run_kwargs(
|
|
602
620
|
agent: Any,
|
|
603
621
|
final_input_text: str,
|
|
@@ -733,6 +751,7 @@ def run(
|
|
|
733
751
|
|
|
734
752
|
parsed_chat_history = _parse_chat_history(chat_history)
|
|
735
753
|
renderer, working_console = _setup_run_renderer(ctx, save, verbose)
|
|
754
|
+
_maybe_attach_transcript_toggle(ctx, renderer)
|
|
736
755
|
|
|
737
756
|
try:
|
|
738
757
|
client.timeout = float(timeout)
|
glaip_sdk/cli/commands/mcps.py
CHANGED
|
@@ -1126,8 +1126,7 @@ def update(
|
|
|
1126
1126
|
)
|
|
1127
1127
|
if not import_file and not cli_flags_provided:
|
|
1128
1128
|
raise click.ClickException(
|
|
1129
|
-
"No update fields specified. Use --import
|
|
1130
|
-
"--name, --transport, --description, --config, --auth"
|
|
1129
|
+
"No update fields specified. Use --import or one of: --name, --transport, --description, --config, --auth"
|
|
1131
1130
|
)
|
|
1132
1131
|
|
|
1133
1132
|
# Resolve MCP using helper function
|
glaip_sdk/cli/slash/session.py
CHANGED
|
@@ -958,14 +958,11 @@ class SlashSession:
|
|
|
958
958
|
keybar = AIPGrid(expand=True)
|
|
959
959
|
keybar.add_column(justify="left", ratio=1)
|
|
960
960
|
keybar.add_column(justify="left", ratio=1)
|
|
961
|
-
keybar.add_column(justify="left", ratio=1)
|
|
962
|
-
keybar.add_column(justify="left", ratio=1)
|
|
963
961
|
|
|
964
962
|
keybar.add_row(
|
|
965
963
|
format_command_hint("/help", "Show commands") or "",
|
|
966
964
|
format_command_hint("/details", "Agent config") or "",
|
|
967
965
|
format_command_hint("/exit", "Back") or "",
|
|
968
|
-
"[bold]Alt+Enter[/bold] [dim]Line break[/dim]",
|
|
969
966
|
)
|
|
970
967
|
|
|
971
968
|
return keybar
|
|
@@ -27,12 +27,18 @@ except Exception: # pragma: no cover - optional dependency
|
|
|
27
27
|
from glaip_sdk.cli.transcript.cache import suggest_filename
|
|
28
28
|
from glaip_sdk.icons import ICON_DELEGATE, ICON_TOOL_STEP
|
|
29
29
|
from glaip_sdk.rich_components import AIPPanel
|
|
30
|
+
from glaip_sdk.utils.rendering.formatting import (
|
|
31
|
+
build_connector_prefix,
|
|
32
|
+
glyph_for_status,
|
|
33
|
+
normalise_display_label,
|
|
34
|
+
)
|
|
30
35
|
from glaip_sdk.utils.rendering.renderer.debug import render_debug_event
|
|
31
36
|
from glaip_sdk.utils.rendering.renderer.panels import create_final_panel
|
|
32
37
|
from glaip_sdk.utils.rendering.renderer.progress import (
|
|
33
38
|
format_elapsed_time,
|
|
34
39
|
is_delegation_tool,
|
|
35
40
|
)
|
|
41
|
+
from glaip_sdk.utils.rendering.steps import StepManager
|
|
36
42
|
|
|
37
43
|
EXPORT_CANCELLED_MESSAGE = "[dim]Export cancelled.[/dim]"
|
|
38
44
|
|
|
@@ -372,12 +378,13 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
372
378
|
self.console.print()
|
|
373
379
|
|
|
374
380
|
def _render_steps_summary(self) -> None:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
+
tree_text = self._build_tree_summary_text()
|
|
382
|
+
if tree_text is not None:
|
|
383
|
+
body = tree_text
|
|
384
|
+
else:
|
|
385
|
+
panel_content = self._format_steps_summary(self._build_step_summary())
|
|
386
|
+
body = Text(panel_content, style="dim")
|
|
387
|
+
panel = AIPPanel(body, title="Steps", border_style="blue")
|
|
381
388
|
self.console.print(panel)
|
|
382
389
|
self.console.print()
|
|
383
390
|
|
|
@@ -460,6 +467,169 @@ class PostRunViewer: # pragma: no cover - interactive flows are not unit tested
|
|
|
460
467
|
|
|
461
468
|
return [steps[name] for name in order]
|
|
462
469
|
|
|
470
|
+
def _build_tree_summary_text(self) -> Text | None:
|
|
471
|
+
"""Render hierarchical tree from captured SSE events when available."""
|
|
472
|
+
manager = StepManager()
|
|
473
|
+
processed = False
|
|
474
|
+
|
|
475
|
+
for event in self.ctx.events:
|
|
476
|
+
payload = self._coerce_step_event(event)
|
|
477
|
+
if not payload:
|
|
478
|
+
continue
|
|
479
|
+
try:
|
|
480
|
+
manager.apply_event(payload)
|
|
481
|
+
processed = True
|
|
482
|
+
except ValueError:
|
|
483
|
+
continue
|
|
484
|
+
|
|
485
|
+
if not processed or not manager.order:
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
lines: list[str] = []
|
|
489
|
+
roots = manager.order
|
|
490
|
+
total_roots = len(roots)
|
|
491
|
+
for index, root_id in enumerate(roots):
|
|
492
|
+
self._render_tree_branch(
|
|
493
|
+
manager=manager,
|
|
494
|
+
step_id=root_id,
|
|
495
|
+
ancestor_state=(),
|
|
496
|
+
is_last=index == total_roots - 1,
|
|
497
|
+
lines=lines,
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if not lines:
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
return Text("\n".join(lines), style="dim")
|
|
504
|
+
|
|
505
|
+
def _render_tree_branch(
|
|
506
|
+
self,
|
|
507
|
+
*,
|
|
508
|
+
manager: StepManager,
|
|
509
|
+
step_id: str,
|
|
510
|
+
ancestor_state: tuple[bool, ...],
|
|
511
|
+
is_last: bool,
|
|
512
|
+
lines: list[str],
|
|
513
|
+
) -> None:
|
|
514
|
+
step = manager.by_id.get(step_id)
|
|
515
|
+
if not step:
|
|
516
|
+
return
|
|
517
|
+
|
|
518
|
+
suppress = self._should_hide_step(step)
|
|
519
|
+
children = manager.children.get(step_id, [])
|
|
520
|
+
|
|
521
|
+
if not suppress:
|
|
522
|
+
branch_state = ancestor_state
|
|
523
|
+
if branch_state:
|
|
524
|
+
branch_state = branch_state + (is_last,)
|
|
525
|
+
lines.append(self._format_tree_line(step, branch_state))
|
|
526
|
+
next_ancestor_state = ancestor_state + (is_last,)
|
|
527
|
+
else:
|
|
528
|
+
next_ancestor_state = ancestor_state
|
|
529
|
+
|
|
530
|
+
if not children:
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
total_children = len(children)
|
|
534
|
+
for idx, child_id in enumerate(children):
|
|
535
|
+
self._render_tree_branch(
|
|
536
|
+
manager=manager,
|
|
537
|
+
step_id=child_id,
|
|
538
|
+
ancestor_state=next_ancestor_state if not suppress else ancestor_state,
|
|
539
|
+
is_last=idx == total_children - 1,
|
|
540
|
+
lines=lines,
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
def _should_hide_step(self, step: Any) -> bool:
|
|
544
|
+
if getattr(step, "parent_id", None) is not None:
|
|
545
|
+
return False
|
|
546
|
+
if getattr(step, "kind", None) == "thinking":
|
|
547
|
+
return True
|
|
548
|
+
if getattr(step, "kind", None) == "agent":
|
|
549
|
+
return True
|
|
550
|
+
name = getattr(step, "name", "") or ""
|
|
551
|
+
return self._looks_like_uuid(name)
|
|
552
|
+
|
|
553
|
+
def _coerce_step_event(self, event: dict[str, Any]) -> dict[str, Any] | None:
|
|
554
|
+
metadata = event.get("metadata")
|
|
555
|
+
if not isinstance(metadata, dict):
|
|
556
|
+
return None
|
|
557
|
+
if not isinstance(metadata.get("step_id"), str):
|
|
558
|
+
return None
|
|
559
|
+
return {
|
|
560
|
+
"metadata": metadata,
|
|
561
|
+
"status": event.get("status"),
|
|
562
|
+
"task_state": event.get("task_state"),
|
|
563
|
+
"content": event.get("content"),
|
|
564
|
+
"task_id": event.get("task_id"),
|
|
565
|
+
"context_id": event.get("context_id"),
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
def _format_tree_line(self, step: Any, branch_state: tuple[bool, ...]) -> str:
|
|
569
|
+
prefix = build_connector_prefix(branch_state)
|
|
570
|
+
raw_label = normalise_display_label(getattr(step, "display_label", None))
|
|
571
|
+
title, summary = self._split_label(raw_label)
|
|
572
|
+
line = f"{prefix}{title}"
|
|
573
|
+
|
|
574
|
+
if summary:
|
|
575
|
+
line += f" — {self._truncate_summary(summary)}"
|
|
576
|
+
|
|
577
|
+
badge = self._format_duration_badge(step)
|
|
578
|
+
if badge:
|
|
579
|
+
line += f" {badge}"
|
|
580
|
+
|
|
581
|
+
glyph = glyph_for_status(getattr(step, "status_icon", None))
|
|
582
|
+
failure_reason = getattr(step, "failure_reason", None)
|
|
583
|
+
if glyph and glyph != "spinner":
|
|
584
|
+
if failure_reason and glyph == "✗":
|
|
585
|
+
line += f" {glyph} {failure_reason}"
|
|
586
|
+
else:
|
|
587
|
+
line += f" {glyph}"
|
|
588
|
+
elif failure_reason:
|
|
589
|
+
line += f" ✗ {failure_reason}"
|
|
590
|
+
|
|
591
|
+
return line
|
|
592
|
+
|
|
593
|
+
@staticmethod
|
|
594
|
+
def _format_duration_badge(step: Any) -> str | None:
|
|
595
|
+
duration_ms = getattr(step, "duration_ms", None)
|
|
596
|
+
if duration_ms is None:
|
|
597
|
+
return None
|
|
598
|
+
try:
|
|
599
|
+
duration_ms = int(duration_ms)
|
|
600
|
+
except Exception:
|
|
601
|
+
return None
|
|
602
|
+
|
|
603
|
+
if duration_ms <= 0:
|
|
604
|
+
payload = "<1ms"
|
|
605
|
+
elif duration_ms >= 1000:
|
|
606
|
+
payload = f"{duration_ms / 1000:.2f}s"
|
|
607
|
+
else:
|
|
608
|
+
payload = f"{duration_ms}ms"
|
|
609
|
+
|
|
610
|
+
return f"[{payload}]"
|
|
611
|
+
|
|
612
|
+
@staticmethod
|
|
613
|
+
def _split_label(label: str) -> tuple[str, str | None]:
|
|
614
|
+
if " — " in label:
|
|
615
|
+
title, summary = label.split(" — ", 1)
|
|
616
|
+
return title.strip(), summary.strip()
|
|
617
|
+
return label.strip(), None
|
|
618
|
+
|
|
619
|
+
@staticmethod
|
|
620
|
+
def _truncate_summary(summary: str, limit: int = 48) -> str:
|
|
621
|
+
summary = summary.strip()
|
|
622
|
+
if len(summary) <= limit:
|
|
623
|
+
return summary
|
|
624
|
+
return summary[: limit - 1].rstrip() + "…"
|
|
625
|
+
|
|
626
|
+
@staticmethod
|
|
627
|
+
def _looks_like_uuid(value: str) -> bool:
|
|
628
|
+
stripped = value.replace("-", "")
|
|
629
|
+
if len(stripped) not in {32, 36}:
|
|
630
|
+
return False
|
|
631
|
+
return all(ch in "0123456789abcdefABCDEF" for ch in stripped)
|
|
632
|
+
|
|
463
633
|
@staticmethod
|
|
464
634
|
def _format_duration_from_ms(value: Any) -> str | None:
|
|
465
635
|
try:
|
glaip_sdk/cli/utils.py
CHANGED
|
@@ -23,6 +23,58 @@ from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
|
|
|
23
23
|
from glaip_sdk.utils.rendering.renderer.config import RendererConfig
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _coerce_to_string(value: Any) -> str:
|
|
27
|
+
"""Return a best-effort string representation for transcripts."""
|
|
28
|
+
try:
|
|
29
|
+
return str(value)
|
|
30
|
+
except Exception:
|
|
31
|
+
return f"{value}"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _has_visible_text(value: Any) -> bool:
|
|
35
|
+
"""Return True when the value is a non-empty string."""
|
|
36
|
+
return isinstance(value, str) and bool(value.strip())
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _update_state_transcript(state: Any, text_value: str) -> bool:
|
|
40
|
+
"""Inject transcript text into renderer state if possible."""
|
|
41
|
+
if state is None:
|
|
42
|
+
return False
|
|
43
|
+
|
|
44
|
+
updated = False
|
|
45
|
+
|
|
46
|
+
if hasattr(state, "final_text") and not _has_visible_text(
|
|
47
|
+
getattr(state, "final_text", "")
|
|
48
|
+
):
|
|
49
|
+
try:
|
|
50
|
+
state.final_text = text_value
|
|
51
|
+
updated = True
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
buffer = getattr(state, "buffer", None)
|
|
56
|
+
if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
|
|
57
|
+
buffer.append(text_value)
|
|
58
|
+
updated = True
|
|
59
|
+
|
|
60
|
+
return updated
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
|
|
64
|
+
"""Populate the renderer (or its state) with the supplied text."""
|
|
65
|
+
state = getattr(renderer, "state", None)
|
|
66
|
+
if _update_state_transcript(state, text_value):
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
if hasattr(renderer, "final_text") and not _has_visible_text(
|
|
70
|
+
getattr(renderer, "final_text", "")
|
|
71
|
+
):
|
|
72
|
+
try:
|
|
73
|
+
setattr(renderer, "final_text", text_value)
|
|
74
|
+
except Exception:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
|
|
26
78
|
class AgentRunRenderingManager:
|
|
27
79
|
"""Coordinate renderer creation and streaming event handling."""
|
|
28
80
|
|
|
@@ -79,7 +131,6 @@ class AgentRunRenderingManager:
|
|
|
79
131
|
silent_config = RendererConfig(
|
|
80
132
|
live=False,
|
|
81
133
|
persist_live=False,
|
|
82
|
-
show_delegate_tool_panels=False,
|
|
83
134
|
render_thinking=False,
|
|
84
135
|
)
|
|
85
136
|
return RichStreamRenderer(
|
|
@@ -92,7 +143,6 @@ class AgentRunRenderingManager:
|
|
|
92
143
|
minimal_config = RendererConfig(
|
|
93
144
|
live=False,
|
|
94
145
|
persist_live=False,
|
|
95
|
-
show_delegate_tool_panels=False,
|
|
96
146
|
render_thinking=False,
|
|
97
147
|
)
|
|
98
148
|
return RichStreamRenderer(
|
|
@@ -106,7 +156,6 @@ class AgentRunRenderingManager:
|
|
|
106
156
|
theme="dark",
|
|
107
157
|
style="debug",
|
|
108
158
|
live=False,
|
|
109
|
-
show_delegate_tool_panels=False,
|
|
110
159
|
append_finished_snapshots=False,
|
|
111
160
|
)
|
|
112
161
|
return RichStreamRenderer(
|
|
@@ -139,17 +188,28 @@ class AgentRunRenderingManager:
|
|
|
139
188
|
|
|
140
189
|
self._capture_request_id(stream_response, meta, renderer)
|
|
141
190
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
191
|
+
controller = getattr(renderer, "transcript_controller", None)
|
|
192
|
+
if controller and getattr(controller, "enabled", False):
|
|
193
|
+
controller.on_stream_start(renderer)
|
|
145
194
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
195
|
+
try:
|
|
196
|
+
for event in iter_sse_events(stream_response, timeout_seconds, agent_name):
|
|
197
|
+
if started_monotonic is None:
|
|
198
|
+
started_monotonic = self._maybe_start_timer(event)
|
|
199
|
+
|
|
200
|
+
final_text, stats_usage = self._process_single_event(
|
|
201
|
+
event,
|
|
202
|
+
renderer,
|
|
203
|
+
final_text,
|
|
204
|
+
stats_usage,
|
|
205
|
+
meta,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if controller and getattr(controller, "enabled", False):
|
|
209
|
+
controller.poll(renderer)
|
|
210
|
+
finally:
|
|
211
|
+
if controller and getattr(controller, "enabled", False):
|
|
212
|
+
controller.on_stream_complete()
|
|
153
213
|
|
|
154
214
|
finished_monotonic = monotonic()
|
|
155
215
|
return final_text, stats_usage, started_monotonic, finished_monotonic
|
|
@@ -194,19 +254,50 @@ class AgentRunRenderingManager:
|
|
|
194
254
|
kind = (ev.get("metadata") or {}).get("kind")
|
|
195
255
|
renderer.on_event(ev)
|
|
196
256
|
|
|
257
|
+
handled = self._handle_metadata_kind(
|
|
258
|
+
kind,
|
|
259
|
+
ev,
|
|
260
|
+
final_text,
|
|
261
|
+
stats_usage,
|
|
262
|
+
meta,
|
|
263
|
+
renderer,
|
|
264
|
+
)
|
|
265
|
+
if handled is not None:
|
|
266
|
+
return handled
|
|
267
|
+
|
|
268
|
+
if ev.get("content"):
|
|
269
|
+
final_text = self._handle_content_event(ev, final_text)
|
|
270
|
+
|
|
271
|
+
return final_text, stats_usage
|
|
272
|
+
|
|
273
|
+
def _handle_metadata_kind(
|
|
274
|
+
self,
|
|
275
|
+
kind: str | None,
|
|
276
|
+
ev: dict[str, Any],
|
|
277
|
+
final_text: str,
|
|
278
|
+
stats_usage: dict[str, Any],
|
|
279
|
+
meta: dict[str, Any],
|
|
280
|
+
renderer: RichStreamRenderer,
|
|
281
|
+
) -> tuple[str, dict[str, Any]] | None:
|
|
282
|
+
"""Process well-known metadata kinds and return updated state."""
|
|
197
283
|
if kind == "artifact":
|
|
198
284
|
return final_text, stats_usage
|
|
199
285
|
|
|
200
|
-
if kind == "final_response"
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
286
|
+
if kind == "final_response":
|
|
287
|
+
content = ev.get("content")
|
|
288
|
+
if content:
|
|
289
|
+
return content, stats_usage
|
|
290
|
+
return final_text, stats_usage
|
|
291
|
+
|
|
292
|
+
if kind == "usage":
|
|
205
293
|
stats_usage.update(ev.get("usage") or {})
|
|
206
|
-
|
|
294
|
+
return final_text, stats_usage
|
|
295
|
+
|
|
296
|
+
if kind == "run_info":
|
|
207
297
|
self._handle_run_info_event(ev, meta, renderer)
|
|
298
|
+
return final_text, stats_usage
|
|
208
299
|
|
|
209
|
-
return
|
|
300
|
+
return None
|
|
210
301
|
|
|
211
302
|
def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
|
|
212
303
|
content = ev.get("content", "")
|
|
@@ -227,6 +318,16 @@ class AgentRunRenderingManager:
|
|
|
227
318
|
meta["run_id"] = ev["run_id"]
|
|
228
319
|
renderer.on_start(meta)
|
|
229
320
|
|
|
321
|
+
def _ensure_renderer_final_content(
|
|
322
|
+
self, renderer: RichStreamRenderer, text: str
|
|
323
|
+
) -> None:
|
|
324
|
+
"""Populate renderer state with final output when the stream omits it."""
|
|
325
|
+
if not text:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
text_value = _coerce_to_string(text)
|
|
329
|
+
_update_renderer_transcript(renderer, text_value)
|
|
330
|
+
|
|
230
331
|
# --------------------------------------------------------------------- #
|
|
231
332
|
# Finalisation helpers
|
|
232
333
|
# --------------------------------------------------------------------- #
|
|
@@ -258,6 +359,10 @@ class AgentRunRenderingManager:
|
|
|
258
359
|
except TypeError:
|
|
259
360
|
rendered_text = ""
|
|
260
361
|
|
|
362
|
+
fallback_text = final_text or rendered_text
|
|
363
|
+
if fallback_text:
|
|
364
|
+
self._ensure_renderer_final_content(renderer, fallback_text)
|
|
365
|
+
|
|
261
366
|
renderer.on_complete(st)
|
|
262
367
|
return final_text or rendered_text or "No response content received."
|
|
263
368
|
|
glaip_sdk/icons.py
CHANGED
|
@@ -5,10 +5,13 @@ Authors:
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
ICON_AGENT = "🤖"
|
|
8
|
-
ICON_AGENT_STEP = "
|
|
8
|
+
ICON_AGENT_STEP = "🤖"
|
|
9
9
|
ICON_TOOL = "🔧"
|
|
10
|
-
ICON_TOOL_STEP = "
|
|
11
|
-
ICON_DELEGATE =
|
|
10
|
+
ICON_TOOL_STEP = "🔧"
|
|
11
|
+
ICON_DELEGATE = ICON_AGENT_STEP
|
|
12
|
+
ICON_STATUS_SUCCESS = "✓"
|
|
13
|
+
ICON_STATUS_FAILED = "✗"
|
|
14
|
+
ICON_STATUS_WARNING = "⚠"
|
|
12
15
|
|
|
13
16
|
__all__ = [
|
|
14
17
|
"ICON_AGENT",
|
|
@@ -16,4 +19,7 @@ __all__ = [
|
|
|
16
19
|
"ICON_TOOL",
|
|
17
20
|
"ICON_TOOL_STEP",
|
|
18
21
|
"ICON_DELEGATE",
|
|
22
|
+
"ICON_STATUS_SUCCESS",
|
|
23
|
+
"ICON_STATUS_FAILED",
|
|
24
|
+
"ICON_STATUS_WARNING",
|
|
19
25
|
]
|
|
@@ -12,7 +12,14 @@ import time
|
|
|
12
12
|
from collections.abc import Callable
|
|
13
13
|
from typing import Any
|
|
14
14
|
|
|
15
|
-
from glaip_sdk.icons import
|
|
15
|
+
from glaip_sdk.icons import (
|
|
16
|
+
ICON_AGENT_STEP,
|
|
17
|
+
ICON_DELEGATE,
|
|
18
|
+
ICON_STATUS_FAILED,
|
|
19
|
+
ICON_STATUS_SUCCESS,
|
|
20
|
+
ICON_STATUS_WARNING,
|
|
21
|
+
ICON_TOOL_STEP,
|
|
22
|
+
)
|
|
16
23
|
|
|
17
24
|
# Constants for argument formatting
|
|
18
25
|
DEFAULT_ARGS_MAX_LEN = 100
|
|
@@ -37,9 +44,20 @@ SECRET_VALUE_PATTERNS = [
|
|
|
37
44
|
re.compile(r"eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+"), # JWT tokens
|
|
38
45
|
]
|
|
39
46
|
SENSITIVE_PATTERNS = re.compile(
|
|
40
|
-
r"password
|
|
47
|
+
r"(?:password|secret|token|key|api_key)(?:\s*[:=]\s*[^\s,}]+)?",
|
|
41
48
|
re.IGNORECASE,
|
|
42
49
|
)
|
|
50
|
+
CONNECTOR_VERTICAL = "│ "
|
|
51
|
+
CONNECTOR_EMPTY = " "
|
|
52
|
+
CONNECTOR_BRANCH = "├─ "
|
|
53
|
+
CONNECTOR_LAST = "└─ "
|
|
54
|
+
ROOT_MARKER = ""
|
|
55
|
+
SECRET_MASK = "••••••"
|
|
56
|
+
STATUS_GLYPHS = {
|
|
57
|
+
"success": ICON_STATUS_SUCCESS,
|
|
58
|
+
"failed": ICON_STATUS_FAILED,
|
|
59
|
+
"warning": ICON_STATUS_WARNING,
|
|
60
|
+
}
|
|
43
61
|
|
|
44
62
|
|
|
45
63
|
def _truncate_string(s: str, max_len: int) -> str:
|
|
@@ -53,7 +71,7 @@ def mask_secrets_in_string(text: str) -> str:
|
|
|
53
71
|
"""Mask sensitive information in a string."""
|
|
54
72
|
result = text
|
|
55
73
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
56
|
-
result = re.sub(pattern,
|
|
74
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
57
75
|
return result
|
|
58
76
|
|
|
59
77
|
|
|
@@ -74,7 +92,7 @@ def _redact_dict_values(text: dict) -> dict:
|
|
|
74
92
|
result = {}
|
|
75
93
|
for key, value in text.items():
|
|
76
94
|
if _is_sensitive_key(key):
|
|
77
|
-
result[key] =
|
|
95
|
+
result[key] = SECRET_MASK
|
|
78
96
|
elif _should_recurse_redaction(value):
|
|
79
97
|
result[key] = redact_sensitive(value)
|
|
80
98
|
else:
|
|
@@ -92,11 +110,11 @@ def _redact_string_content(text: str) -> str:
|
|
|
92
110
|
result = text
|
|
93
111
|
# First mask secrets
|
|
94
112
|
for pattern in SECRET_VALUE_PATTERNS:
|
|
95
|
-
result = re.sub(pattern,
|
|
113
|
+
result = re.sub(pattern, SECRET_MASK, result)
|
|
96
114
|
# Then redact sensitive patterns
|
|
97
115
|
result = re.sub(
|
|
98
116
|
SENSITIVE_PATTERNS,
|
|
99
|
-
lambda m: m.group(0).split("=")[0] + "
|
|
117
|
+
lambda m: m.group(0).split("=")[0] + "=" + SECRET_MASK,
|
|
100
118
|
result,
|
|
101
119
|
)
|
|
102
120
|
return result
|
|
@@ -116,6 +134,31 @@ def _should_recurse_redaction(value: Any) -> bool:
|
|
|
116
134
|
return isinstance(value, dict | list) or isinstance(value, str)
|
|
117
135
|
|
|
118
136
|
|
|
137
|
+
def glyph_for_status(icon_key: str | None) -> str | None:
|
|
138
|
+
"""Return glyph representing a step status icon key."""
|
|
139
|
+
if not icon_key:
|
|
140
|
+
return None
|
|
141
|
+
return STATUS_GLYPHS.get(icon_key)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def normalise_display_label(label: str | None) -> str:
|
|
145
|
+
"""Return a user facing label or the Unknown fallback."""
|
|
146
|
+
label = (label or "").strip()
|
|
147
|
+
return label or "Unknown step detail"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_connector_prefix(branch_state: tuple[bool, ...]) -> str:
|
|
151
|
+
"""Build connector prefix for a tree line based on ancestry state."""
|
|
152
|
+
if not branch_state:
|
|
153
|
+
return ROOT_MARKER
|
|
154
|
+
|
|
155
|
+
parts: list[str] = []
|
|
156
|
+
for ancestor_is_last in branch_state[:-1]:
|
|
157
|
+
parts.append(CONNECTOR_EMPTY if ancestor_is_last else CONNECTOR_VERTICAL)
|
|
158
|
+
parts.append(CONNECTOR_LAST if branch_state[-1] else CONNECTOR_BRANCH)
|
|
159
|
+
return "".join(parts)
|
|
160
|
+
|
|
161
|
+
|
|
119
162
|
def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
120
163
|
"""Format arguments in a pretty way."""
|
|
121
164
|
if not args:
|
|
@@ -132,7 +175,7 @@ def pretty_args(args: dict | None, max_len: int = DEFAULT_ARGS_MAX_LEN) -> str:
|
|
|
132
175
|
try:
|
|
133
176
|
args_str = json.dumps(masked_args, ensure_ascii=False, separators=(",", ":"))
|
|
134
177
|
return _truncate_string(args_str, max_len)
|
|
135
|
-
except
|
|
178
|
+
except Exception:
|
|
136
179
|
# Fallback to string representation if JSON serialization fails
|
|
137
180
|
args_str = str(masked_args)
|
|
138
181
|
return _truncate_string(args_str, max_len)
|
|
@@ -30,19 +30,32 @@ class Step:
|
|
|
30
30
|
context_id: str | None = None
|
|
31
31
|
started_at: float = field(default_factory=monotonic)
|
|
32
32
|
duration_ms: int | None = None
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
duration_source: str | None = None
|
|
34
|
+
display_label: str | None = None
|
|
35
|
+
status_icon: str | None = None
|
|
36
|
+
failure_reason: str | None = None
|
|
37
|
+
branch_failed: bool = False
|
|
38
|
+
is_parallel: bool = False
|
|
39
|
+
server_started_at: float | None = None
|
|
40
|
+
server_finished_at: float | None = None
|
|
41
|
+
duration_unknown: bool = False
|
|
42
|
+
|
|
43
|
+
def finish(self, duration_raw: float | None, *, source: str | None = None) -> None:
|
|
35
44
|
"""Mark the step as finished and calculate duration.
|
|
36
45
|
|
|
37
46
|
Args:
|
|
38
47
|
duration_raw: Raw duration in seconds, or None to calculate from started_at
|
|
48
|
+
source: Optional duration source tag
|
|
39
49
|
"""
|
|
50
|
+
self.duration_unknown = False
|
|
40
51
|
if isinstance(duration_raw, int | float) and duration_raw > 0:
|
|
41
52
|
# Use provided duration if it's a positive number (even if very small)
|
|
42
53
|
self.duration_ms = round(float(duration_raw) * 1000)
|
|
54
|
+
self.duration_source = source or self.duration_source or "provided"
|
|
43
55
|
else:
|
|
44
56
|
# Calculate from started_at if duration_raw is None, negative, or zero
|
|
45
57
|
self.duration_ms = int((monotonic() - self.started_at) * 1000)
|
|
58
|
+
self.duration_source = source or self.duration_source or "monotonic"
|
|
46
59
|
self.status = "finished"
|
|
47
60
|
|
|
48
61
|
|
|
@@ -35,7 +35,6 @@ def make_silent_renderer() -> RichStreamRenderer:
|
|
|
35
35
|
cfg = RendererConfig(
|
|
36
36
|
live=False,
|
|
37
37
|
persist_live=False,
|
|
38
|
-
show_delegate_tool_panels=False,
|
|
39
38
|
render_thinking=False,
|
|
40
39
|
)
|
|
41
40
|
return RichStreamRenderer(
|
|
@@ -51,7 +50,6 @@ def make_minimal_renderer() -> RichStreamRenderer:
|
|
|
51
50
|
cfg = RendererConfig(
|
|
52
51
|
live=False,
|
|
53
52
|
persist_live=False,
|
|
54
|
-
show_delegate_tool_panels=False,
|
|
55
53
|
render_thinking=False,
|
|
56
54
|
)
|
|
57
55
|
return RichStreamRenderer(console=Console(), cfg=cfg)
|