glaip-sdk 0.3.0__py3-none-any.whl → 0.4.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.
Files changed (50) hide show
  1. glaip_sdk/cli/auth.py +2 -1
  2. glaip_sdk/cli/commands/agents.py +1 -1
  3. glaip_sdk/cli/commands/configure.py +2 -1
  4. glaip_sdk/cli/commands/mcps.py +191 -44
  5. glaip_sdk/cli/commands/transcripts.py +1 -1
  6. glaip_sdk/cli/display.py +1 -1
  7. glaip_sdk/cli/hints.py +58 -0
  8. glaip_sdk/cli/io.py +6 -3
  9. glaip_sdk/cli/main.py +2 -1
  10. glaip_sdk/cli/slash/agent_session.py +2 -1
  11. glaip_sdk/cli/slash/session.py +1 -1
  12. glaip_sdk/cli/transcript/capture.py +1 -1
  13. glaip_sdk/cli/transcript/viewer.py +13 -646
  14. glaip_sdk/cli/update_notifier.py +2 -1
  15. glaip_sdk/cli/utils.py +63 -110
  16. glaip_sdk/client/agents.py +2 -4
  17. glaip_sdk/client/main.py +2 -18
  18. glaip_sdk/client/mcps.py +11 -1
  19. glaip_sdk/client/run_rendering.py +90 -111
  20. glaip_sdk/client/shared.py +21 -0
  21. glaip_sdk/models.py +8 -7
  22. glaip_sdk/utils/display.py +23 -15
  23. glaip_sdk/utils/rendering/__init__.py +6 -13
  24. glaip_sdk/utils/rendering/formatting.py +5 -30
  25. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  26. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  27. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  28. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  29. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  30. glaip_sdk/utils/rendering/models.py +1 -0
  31. glaip_sdk/utils/rendering/renderer/__init__.py +10 -28
  32. glaip_sdk/utils/rendering/renderer/base.py +214 -1469
  33. glaip_sdk/utils/rendering/renderer/debug.py +24 -0
  34. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  35. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  36. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  37. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  38. glaip_sdk/utils/rendering/state.py +204 -0
  39. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  40. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  41. glaip_sdk/utils/rendering/steps/format.py +176 -0
  42. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  43. glaip_sdk/utils/rendering/timing.py +36 -0
  44. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  45. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  46. glaip_sdk/utils/validation.py +13 -21
  47. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/METADATA +1 -1
  48. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/RECORD +50 -34
  49. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/WHEEL +0 -0
  50. {glaip_sdk-0.3.0.dist-info → glaip_sdk-0.4.0.dist-info}/entry_points.txt +0 -0
@@ -26,7 +26,8 @@ from glaip_sdk.branding import (
26
26
  )
27
27
  from glaip_sdk.cli.commands.update import update_command
28
28
  from glaip_sdk.cli.constants import UPDATE_CHECK_ENABLED
29
- from glaip_sdk.cli.utils import command_hint, format_command_hint
29
+ from glaip_sdk.cli.hints import format_command_hint
30
+ from glaip_sdk.cli.utils import command_hint
30
31
  from glaip_sdk.rich_components import AIPPanel
31
32
 
32
33
  FetchLatestVersion = Callable[[], str | None]
glaip_sdk/cli/utils.py CHANGED
@@ -7,12 +7,12 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
+ import asyncio
10
11
  import importlib
11
12
  import json
12
13
  import logging
13
14
  import os
14
15
  import sys
15
- import asyncio
16
16
  from collections.abc import Callable, Iterable
17
17
  from contextlib import AbstractContextManager, contextmanager, nullcontext
18
18
  from pathlib import Path
@@ -27,19 +27,21 @@ from rich.syntax import Syntax
27
27
  from glaip_sdk import _version as _version_module
28
28
  from glaip_sdk.branding import (
29
29
  ACCENT_STYLE,
30
- HINT_COMMAND_STYLE,
31
- HINT_DESCRIPTION_COLOR,
32
30
  SUCCESS_STYLE,
33
31
  WARNING_STYLE,
34
32
  )
35
33
  from glaip_sdk.cli import masking, pager
36
- from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
37
34
  from glaip_sdk.cli.config import load_config
35
+ from glaip_sdk.cli.constants import LITERAL_STRING_THRESHOLD, TABLE_SORT_ENABLED
38
36
  from glaip_sdk.cli.context import (
39
37
  _get_view,
40
- detect_export_format as _detect_export_format,
41
38
  get_ctx_value,
42
39
  )
40
+ from glaip_sdk.cli.context import (
41
+ detect_export_format as _detect_export_format,
42
+ )
43
+ from glaip_sdk.cli import display as cli_display
44
+ from glaip_sdk.cli.hints import command_hint
43
45
  from glaip_sdk.cli.io import export_resource_to_file_with_validation
44
46
  from glaip_sdk.cli.rich_helpers import markup_text, print_markup
45
47
  from glaip_sdk.icons import ICON_AGENT
@@ -47,10 +49,35 @@ from glaip_sdk.rich_components import AIPPanel, AIPTable
47
49
  from glaip_sdk.utils import format_datetime, is_uuid
48
50
  from glaip_sdk.utils.rendering.renderer import (
49
51
  CapturingConsole,
50
- RendererConfig,
52
+ RendererFactoryOptions,
51
53
  RichStreamRenderer,
54
+ make_default_renderer,
55
+ make_verbose_renderer,
52
56
  )
53
57
 
58
+ questionary = None # type: ignore[assignment]
59
+
60
+
61
+ def _load_questionary_module() -> tuple[Any | None, Any | None]:
62
+ """Return the questionary module and Choice class if available."""
63
+ module = questionary
64
+ if module is not None:
65
+ return module, getattr(module, "Choice", None)
66
+
67
+ try: # pragma: no cover - optional dependency
68
+ module = __import__("questionary")
69
+ except ImportError:
70
+ return None, None
71
+
72
+ return module, getattr(module, "Choice", None)
73
+
74
+
75
+ def _make_questionary_choice(choice_cls: Any | None, **kwargs: Any) -> Any:
76
+ """Create a questionary Choice instance or lightweight fallback."""
77
+ if choice_cls is None:
78
+ return kwargs
79
+ return choice_cls(**kwargs)
80
+
54
81
 
55
82
  @contextmanager
56
83
  def bind_slash_session_context(ctx: Any, session: Any) -> Any:
@@ -122,31 +149,28 @@ def prompt_export_choice_questionary(
122
149
  Tuple of (choice, path) or None if cancelled/unavailable.
123
150
  Choice can be "default", "custom", or "cancel".
124
151
  """
125
- # Import here for optional dependency (questionary may not be installed)
126
- try: # pragma: no cover - optional dependency
127
- import questionary
128
- from questionary import Choice
129
- except Exception: # pragma: no cover - optional dependency
130
- return None
131
-
132
- if questionary is None or Choice is None:
152
+ questionary_module, choice_cls = _load_questionary_module()
153
+ if questionary_module is None or choice_cls is None:
133
154
  return None
134
155
 
135
156
  try:
136
- question = questionary.select(
157
+ question = questionary_module.select(
137
158
  "Export transcript",
138
159
  choices=[
139
- Choice(
160
+ _make_questionary_choice(
161
+ choice_cls,
140
162
  title=f"Save to default ({default_display})",
141
163
  value=("default", default_path),
142
164
  shortcut_key="1",
143
165
  ),
144
- Choice(
166
+ _make_questionary_choice(
167
+ choice_cls,
145
168
  title="Choose a different path",
146
169
  value=("custom", None),
147
170
  shortcut_key="2",
148
171
  ),
149
- Choice(
172
+ _make_questionary_choice(
173
+ choice_cls,
150
174
  title="Cancel",
151
175
  value=("cancel", None),
152
176
  shortcut_key="3",
@@ -194,9 +218,7 @@ def _run_questionary_in_thread(question: Any, *, patch_stdout: bool = False) ->
194
218
  run_callable = getattr(application, "run", None) if application is not None else None
195
219
  if callable(run_callable):
196
220
  try:
197
- if patch_stdout:
198
- from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
199
-
221
+ if patch_stdout and pt_patch_stdout is not None:
200
222
  with pt_patch_stdout():
201
223
  return run_callable(in_thread=True)
202
224
  return run_callable(in_thread=True)
@@ -226,6 +248,7 @@ _LiteralYamlDumper.add_representer(str, _literal_str_representer)
226
248
  try:
227
249
  from prompt_toolkit.buffer import Buffer
228
250
  from prompt_toolkit.completion import Completion
251
+ from prompt_toolkit.patch_stdout import patch_stdout as pt_patch_stdout
229
252
  from prompt_toolkit.selection import SelectionType
230
253
  from prompt_toolkit.shortcuts import PromptSession, prompt
231
254
 
@@ -235,13 +258,9 @@ except Exception: # pragma: no cover - optional dependency
235
258
  SelectionType = None # type: ignore[assignment]
236
259
  PromptSession = None # type: ignore[assignment]
237
260
  prompt = None # type: ignore[assignment]
261
+ pt_patch_stdout = None # type: ignore[assignment]
238
262
  _HAS_PTK = False
239
263
 
240
- try:
241
- import questionary
242
- except Exception: # pragma: no cover - optional dependency
243
- questionary = None
244
-
245
264
  if TYPE_CHECKING: # pragma: no cover - import-only during type checking
246
265
  from glaip_sdk import Client
247
266
 
@@ -401,9 +420,7 @@ def handle_resource_export(
401
420
  ):
402
421
  export_resource_to_file_with_validation(full_resource, export_path, detected_format)
403
422
  except Exception:
404
- from glaip_sdk.cli.display import handle_rich_output # noqa: E402 - avoid circular import
405
-
406
- handle_rich_output(
423
+ cli_display.handle_rich_output(
407
424
  ctx,
408
425
  markup_text(f"[{WARNING_STYLE}]⚠️ Failed to fetch full details, using available data[/]"),
409
426
  )
@@ -416,73 +433,6 @@ def handle_resource_export(
416
433
  )
417
434
 
418
435
 
419
- def in_slash_mode(ctx: click.Context | None = None) -> bool:
420
- """Return True when running inside the slash command palette."""
421
- if ctx is None:
422
- try:
423
- ctx = click.get_current_context(silent=True)
424
- except RuntimeError:
425
- ctx = None
426
-
427
- if ctx is None:
428
- return False
429
-
430
- obj = getattr(ctx, "obj", None)
431
- if isinstance(obj, dict):
432
- return bool(obj.get("_slash_session"))
433
-
434
- return bool(getattr(obj, "_slash_session", False))
435
-
436
-
437
- def command_hint(
438
- cli_command: str | None,
439
- slash_command: str | None = None,
440
- *,
441
- ctx: click.Context | None = None,
442
- ) -> str | None:
443
- """Return the appropriate command string for the current mode.
444
-
445
- Args:
446
- cli_command: Command string without the ``aip`` prefix (e.g., ``"status"``).
447
- slash_command: Slash command counterpart (e.g., ``"status"`` or ``"/status"``).
448
- ctx: Optional Click context override.
449
-
450
- Returns:
451
- The formatted command string for the active mode, or ``None`` when no
452
- equivalent command exists in that mode.
453
- """
454
- if in_slash_mode(ctx):
455
- if not slash_command:
456
- return None
457
- return slash_command if slash_command.startswith("/") else f"/{slash_command}"
458
-
459
- if not cli_command:
460
- return None
461
- return f"aip {cli_command}"
462
-
463
-
464
- def format_command_hint(
465
- command: str | None,
466
- description: str | None = None,
467
- ) -> str | None:
468
- """Return a Rich markup string that highlights a command hint.
469
-
470
- Args:
471
- command: Command text to highlight (already formatted for the active mode).
472
- description: Optional short description to display alongside the command.
473
-
474
- Returns:
475
- Markup string suitable for Rich rendering, or ``None`` when ``command`` is falsy.
476
- """
477
- if not command:
478
- return None
479
-
480
- highlighted = f"[{HINT_COMMAND_STYLE}]{command}[/]"
481
- if description:
482
- highlighted += f" [{HINT_DESCRIPTION_COLOR}]{description}[/{HINT_DESCRIPTION_COLOR}]"
483
- return highlighted
484
-
485
-
486
436
  def sdk_version() -> str:
487
437
  """Return the current SDK version, warning if metadata is unavailable."""
488
438
  version = getattr(_version_module, "__version__", None)
@@ -1358,19 +1308,20 @@ def build_renderer(
1358
1308
 
1359
1309
  # Configure renderer based on verbose mode and explicit overrides
1360
1310
  live_enabled = bool(live) if live is not None else not verbose
1361
- renderer_cfg = RendererConfig(
1362
- live=live_enabled,
1363
- append_finished_snapshots=bool(snapshots)
1364
- if snapshots is not None
1365
- else RendererConfig.append_finished_snapshots,
1311
+ cfg_overrides = {
1312
+ "live": live_enabled,
1313
+ "append_finished_snapshots": bool(snapshots) if snapshots is not None else False,
1314
+ }
1315
+ renderer_console = (
1316
+ working_console.original_console if isinstance(working_console, CapturingConsole) else working_console
1366
1317
  )
1367
-
1368
- # Create the renderer instance
1369
- renderer = RichStreamRenderer(
1370
- working_console.original_console if isinstance(working_console, CapturingConsole) else working_console,
1371
- cfg=renderer_cfg,
1372
- verbose=verbose,
1318
+ factory = make_verbose_renderer if verbose else make_default_renderer
1319
+ factory_options = RendererFactoryOptions(
1320
+ console=renderer_console,
1321
+ cfg_overrides=cfg_overrides,
1322
+ verbose=verbose if factory is make_default_renderer else None,
1373
1323
  )
1324
+ renderer = factory_options.build(factory)
1374
1325
 
1375
1326
  # Link the renderer back to the slash session when running from the palette.
1376
1327
  _register_renderer_with_session(_ctx, renderer)
@@ -1540,17 +1491,19 @@ def _handle_json_view_ambiguity(matches: list[Any]) -> Any:
1540
1491
 
1541
1492
  def _handle_questionary_ambiguity(resource_type: str, ref: str, matches: list[Any]) -> Any:
1542
1493
  """Handle ambiguity using questionary interactive interface."""
1543
- if not (questionary and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
1494
+ questionary_module, choice_cls = _load_questionary_module()
1495
+ if not (questionary_module and os.getenv("TERM") and os.isatty(0) and os.isatty(1)):
1544
1496
  raise click.ClickException("Interactive selection not available")
1545
1497
 
1546
1498
  # Escape special characters for questionary
1547
1499
  safe_resource_type = resource_type.replace("{", "{{").replace("}", "}}")
1548
1500
  safe_ref = ref.replace("{", "{{").replace("}", "}}")
1549
1501
 
1550
- picked_idx = questionary.select(
1502
+ picked_idx = questionary_module.select(
1551
1503
  f"Multiple {safe_resource_type}s match '{safe_ref}'. Pick one:",
1552
1504
  choices=[
1553
- questionary.Choice(
1505
+ _make_questionary_choice(
1506
+ choice_cls,
1554
1507
  title=(
1555
1508
  f"{getattr(m, 'name', '—').replace('{', '{{').replace('}', '}}')} — "
1556
1509
  f"{getattr(m, 'id', '').replace('{', '{{').replace('}', '}}')}"
@@ -29,6 +29,7 @@ from glaip_sdk.client.run_rendering import (
29
29
  AgentRunRenderingManager,
30
30
  compute_timeout_seconds,
31
31
  )
32
+ from glaip_sdk.client.shared import build_shared_config
32
33
  from glaip_sdk.client.tools import ToolClient
33
34
  from glaip_sdk.config.constants import (
34
35
  AGENT_CONFIG_FIELDS,
@@ -1235,9 +1236,6 @@ class AgentClient(BaseClient):
1235
1236
  def runs(self) -> "AgentRunsClient":
1236
1237
  """Get the agent runs client."""
1237
1238
  if self._runs_client is None:
1238
- # Import here to avoid circular dependency with client.main
1239
- from glaip_sdk.client.main import _build_shared_config
1240
-
1241
- shared_config = _build_shared_config(self)
1239
+ shared_config = build_shared_config(self)
1242
1240
  self._runs_client = AgentRunsClient(**shared_config)
1243
1241
  return self._runs_client
glaip_sdk/client/main.py CHANGED
@@ -10,27 +10,11 @@ from typing import Any
10
10
  from glaip_sdk.client.agents import AgentClient
11
11
  from glaip_sdk.client.base import BaseClient
12
12
  from glaip_sdk.client.mcps import MCPClient
13
+ from glaip_sdk.client.shared import build_shared_config
13
14
  from glaip_sdk.client.tools import ToolClient
14
15
  from glaip_sdk.models import MCP, Agent, Tool
15
16
 
16
17
 
17
- def _build_shared_config(client: BaseClient) -> dict[str, Any]:
18
- """Build shared configuration dictionary for sub-clients.
19
-
20
- Args:
21
- client: Base client instance.
22
-
23
- Returns:
24
- Dictionary with shared configuration.
25
- """
26
- return {
27
- "parent_client": client,
28
- "api_url": client.api_url,
29
- "api_key": client.api_key,
30
- "timeout": client._timeout,
31
- }
32
-
33
-
34
18
  class Client(BaseClient):
35
19
  """Main client that composes all specialized clients and shares one HTTP session."""
36
20
 
@@ -42,7 +26,7 @@ class Client(BaseClient):
42
26
  """
43
27
  super().__init__(**kwargs)
44
28
  # Share the single httpx.Client + config with sub-clients
45
- shared_config = _build_shared_config(self)
29
+ shared_config = build_shared_config(self)
46
30
  self.agents = AgentClient(**shared_config)
47
31
  self.tools = ToolClient(**shared_config)
48
32
  self.mcps = MCPClient(**shared_config)
glaip_sdk/client/mcps.py CHANGED
@@ -204,7 +204,17 @@ class MCPClient(BaseClient):
204
204
  def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
205
205
  """Get tools available from an MCP."""
206
206
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
207
- return data or []
207
+ if data is None:
208
+ return []
209
+ if isinstance(data, list):
210
+ return data
211
+ if isinstance(data, dict):
212
+ if "tools" in data:
213
+ return data.get("tools", []) or []
214
+ logger.warning("Unexpected MCP tools response keys %s; returning empty list", list(data.keys()))
215
+ return []
216
+ logger.warning("Unexpected MCP tools response type %s; returning empty list", type(data).__name__)
217
+ return []
208
218
 
209
219
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
210
220
  """Test MCP connection using configuration.
@@ -7,9 +7,9 @@ Authors:
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import io
11
10
  import json
12
11
  import logging
12
+ from collections.abc import Callable
13
13
  from time import monotonic
14
14
  from typing import Any
15
15
 
@@ -19,8 +19,17 @@ from rich.console import Console as _Console
19
19
  from glaip_sdk.config.constants import DEFAULT_AGENT_RUN_TIMEOUT
20
20
  from glaip_sdk.utils.client_utils import iter_sse_events
21
21
  from glaip_sdk.utils.rendering.models import RunStats
22
- from glaip_sdk.utils.rendering.renderer import RichStreamRenderer
23
- from glaip_sdk.utils.rendering.renderer.config import RendererConfig
22
+ from glaip_sdk.utils.rendering.renderer import (
23
+ RendererFactoryOptions,
24
+ RichStreamRenderer,
25
+ make_default_renderer,
26
+ make_minimal_renderer,
27
+ make_silent_renderer,
28
+ make_verbose_renderer,
29
+ )
30
+ from glaip_sdk.utils.rendering.state import TranscriptBuffer
31
+
32
+ NO_AGENT_RESPONSE_FALLBACK = "No agent response received."
24
33
 
25
34
 
26
35
  def _coerce_to_string(value: Any) -> str:
@@ -36,41 +45,6 @@ def _has_visible_text(value: Any) -> bool:
36
45
  return isinstance(value, str) and bool(value.strip())
37
46
 
38
47
 
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(getattr(state, "final_text", "")):
47
- try:
48
- state.final_text = text_value
49
- updated = True
50
- except Exception:
51
- pass
52
-
53
- buffer = getattr(state, "buffer", None)
54
- if isinstance(buffer, list) and not any(_has_visible_text(item) for item in buffer):
55
- buffer.append(text_value)
56
- updated = True
57
-
58
- return updated
59
-
60
-
61
- def _update_renderer_transcript(renderer: Any, text_value: str) -> None:
62
- """Populate the renderer (or its state) with the supplied text."""
63
- state = getattr(renderer, "state", None)
64
- if _update_state_transcript(state, text_value):
65
- return
66
-
67
- if hasattr(renderer, "final_text") and not _has_visible_text(getattr(renderer, "final_text", "")):
68
- try:
69
- renderer.final_text = text_value
70
- except Exception:
71
- pass
72
-
73
-
74
48
  class AgentRunRenderingManager:
75
49
  """Coordinate renderer creation and streaming event handling."""
76
50
 
@@ -81,6 +55,7 @@ class AgentRunRenderingManager:
81
55
  logger: Optional logger instance, creates default if None
82
56
  """
83
57
  self._logger = logger or logging.getLogger(__name__)
58
+ self._buffer_factory = TranscriptBuffer
84
59
 
85
60
  # --------------------------------------------------------------------- #
86
61
  # Renderer setup helpers
@@ -92,17 +67,38 @@ class AgentRunRenderingManager:
92
67
  verbose: bool = False,
93
68
  ) -> RichStreamRenderer:
94
69
  """Create an appropriate renderer based on the supplied spec."""
70
+ transcript_buffer = self._buffer_factory()
71
+ base_options = RendererFactoryOptions(console=_Console(), transcript_buffer=transcript_buffer)
95
72
  if isinstance(renderer_spec, RichStreamRenderer):
96
73
  return renderer_spec
97
74
 
98
75
  if isinstance(renderer_spec, str):
99
- if renderer_spec == "silent":
100
- return self._create_silent_renderer()
101
- if renderer_spec == "minimal":
102
- return self._create_minimal_renderer()
103
- return self._create_default_renderer(verbose)
76
+ lowered = renderer_spec.lower()
77
+ if lowered == "silent":
78
+ return self._attach_buffer(base_options.build(make_silent_renderer), transcript_buffer)
79
+ if lowered == "minimal":
80
+ return self._attach_buffer(base_options.build(make_minimal_renderer), transcript_buffer)
81
+ if lowered == "verbose":
82
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
83
+
84
+ if verbose:
85
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
104
86
 
105
- return self._create_default_renderer(verbose)
87
+ default_options = RendererFactoryOptions(
88
+ console=_Console(),
89
+ transcript_buffer=transcript_buffer,
90
+ verbose=verbose,
91
+ )
92
+ return self._attach_buffer(default_options.build(make_default_renderer), transcript_buffer)
93
+
94
+ @staticmethod
95
+ def _attach_buffer(renderer: RichStreamRenderer, buffer: TranscriptBuffer) -> RichStreamRenderer:
96
+ """Attach a captured transcript buffer to a renderer for later inspection."""
97
+ try:
98
+ renderer._captured_transcript_buffer = buffer # type: ignore[attr-defined]
99
+ except Exception:
100
+ pass
101
+ return renderer
106
102
 
107
103
  def build_initial_metadata(
108
104
  self,
@@ -123,70 +119,6 @@ class AgentRunRenderingManager:
123
119
  """Notify renderer that streaming is starting."""
124
120
  renderer.on_start(meta)
125
121
 
126
- def _create_silent_renderer(self) -> RichStreamRenderer:
127
- """Create a silent renderer that outputs to a string buffer.
128
-
129
- Returns:
130
- RichStreamRenderer configured for silent output.
131
- """
132
- silent_config = RendererConfig(
133
- live=False,
134
- persist_live=False,
135
- render_thinking=False,
136
- )
137
- return RichStreamRenderer(
138
- console=_Console(file=io.StringIO(), force_terminal=False),
139
- cfg=silent_config,
140
- verbose=False,
141
- )
142
-
143
- def _create_minimal_renderer(self) -> RichStreamRenderer:
144
- """Create a minimal renderer with reduced output.
145
-
146
- Returns:
147
- RichStreamRenderer configured for minimal output.
148
- """
149
- minimal_config = RendererConfig(
150
- live=False,
151
- persist_live=False,
152
- render_thinking=False,
153
- )
154
- return RichStreamRenderer(
155
- console=_Console(),
156
- cfg=minimal_config,
157
- verbose=False,
158
- )
159
-
160
- def _create_verbose_renderer(self) -> RichStreamRenderer:
161
- """Create a verbose renderer with detailed output.
162
-
163
- Returns:
164
- RichStreamRenderer configured for verbose output.
165
- """
166
- verbose_config = RendererConfig(
167
- live=False,
168
- append_finished_snapshots=False,
169
- )
170
- return RichStreamRenderer(
171
- console=_Console(),
172
- cfg=verbose_config,
173
- verbose=True,
174
- )
175
-
176
- def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
177
- """Create the default renderer based on verbosity.
178
-
179
- Args:
180
- verbose: Whether to create a verbose renderer.
181
-
182
- Returns:
183
- RichStreamRenderer instance.
184
- """
185
- if verbose:
186
- return self._create_verbose_renderer()
187
- default_config = RendererConfig()
188
- return RichStreamRenderer(console=_Console(), cfg=default_config)
189
-
190
122
  # --------------------------------------------------------------------- #
191
123
  # Streaming event handling
192
124
  # --------------------------------------------------------------------- #
@@ -382,7 +314,52 @@ class AgentRunRenderingManager:
382
314
  return
383
315
 
384
316
  text_value = _coerce_to_string(text)
385
- _update_renderer_transcript(renderer, text_value)
317
+ state = getattr(renderer, "state", None)
318
+ if state is None:
319
+ self._ensure_renderer_text(renderer, text_value)
320
+ return
321
+
322
+ self._ensure_state_final_text(state, text_value)
323
+ self._ensure_state_buffer(state, text_value)
324
+
325
+ def _ensure_renderer_text(self, renderer: RichStreamRenderer, text_value: str) -> None:
326
+ """Best-effort assignment for renderer.final_text."""
327
+ if not hasattr(renderer, "final_text"):
328
+ return
329
+ current_text = getattr(renderer, "final_text", "")
330
+ if _has_visible_text(current_text):
331
+ return
332
+ self._safe_set_attr(renderer, "final_text", text_value)
333
+
334
+ def _ensure_state_final_text(self, state: Any, text_value: str) -> None:
335
+ """Best-effort assignment for renderer.state.final_text."""
336
+ current_text = getattr(state, "final_text", "")
337
+ if _has_visible_text(current_text):
338
+ return
339
+ self._safe_set_attr(state, "final_text", text_value)
340
+
341
+ def _ensure_state_buffer(self, state: Any, text_value: str) -> None:
342
+ """Append fallback text to the state buffer when available."""
343
+ buffer = getattr(state, "buffer", None)
344
+ if not hasattr(buffer, "append"):
345
+ return
346
+ self._safe_append(buffer.append, text_value)
347
+
348
+ @staticmethod
349
+ def _safe_set_attr(target: Any, attr: str, value: str) -> None:
350
+ """Assign attribute while masking renderer-specific failures."""
351
+ try:
352
+ setattr(target, attr, value)
353
+ except Exception:
354
+ pass
355
+
356
+ @staticmethod
357
+ def _safe_append(appender: Callable[[str], Any], value: str) -> None:
358
+ """Invoke append-like functions without leaking renderer errors."""
359
+ try:
360
+ appender(value)
361
+ except Exception:
362
+ pass
386
363
 
387
364
  # --------------------------------------------------------------------- #
388
365
  # Finalisation helpers
@@ -409,7 +386,9 @@ class AgentRunRenderingManager:
409
386
  elif hasattr(renderer, "buffer"):
410
387
  buffer_values = renderer.buffer
411
388
 
412
- if buffer_values is not None:
389
+ if isinstance(buffer_values, TranscriptBuffer):
390
+ rendered_text = buffer_values.render()
391
+ elif buffer_values is not None:
413
392
  try:
414
393
  rendered_text = "".join(buffer_values)
415
394
  except TypeError:
@@ -420,7 +399,7 @@ class AgentRunRenderingManager:
420
399
  self._ensure_renderer_final_content(renderer, fallback_text)
421
400
 
422
401
  renderer.on_complete(st)
423
- return final_text or rendered_text or "No response content received."
402
+ return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
424
403
 
425
404
 
426
405
  def compute_timeout_seconds(kwargs: dict[str, Any]) -> float:
@@ -0,0 +1,21 @@
1
+ """Shared helpers for client configuration wiring.
2
+
3
+ Authors:
4
+ Raymond Christopher (raymond.christopher@gdplabs.id)
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ from glaip_sdk.client.base import BaseClient
12
+
13
+
14
+ def build_shared_config(client: BaseClient) -> dict[str, Any]:
15
+ """Return the keyword arguments used to initialize sub-clients."""
16
+ return {
17
+ "parent_client": client,
18
+ "api_url": client.api_url,
19
+ "api_key": client.api_key,
20
+ "timeout": client._timeout,
21
+ }