glaip-sdk 0.1.2__py3-none-any.whl → 0.6.5b3__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 (129) hide show
  1. glaip_sdk/__init__.py +5 -2
  2. glaip_sdk/_version.py +9 -0
  3. glaip_sdk/agents/__init__.py +27 -0
  4. glaip_sdk/agents/base.py +1090 -0
  5. glaip_sdk/branding.py +13 -0
  6. glaip_sdk/cli/account_store.py +540 -0
  7. glaip_sdk/cli/auth.py +254 -15
  8. glaip_sdk/cli/commands/__init__.py +2 -2
  9. glaip_sdk/cli/commands/accounts.py +746 -0
  10. glaip_sdk/cli/commands/agents.py +214 -74
  11. glaip_sdk/cli/commands/common_config.py +101 -0
  12. glaip_sdk/cli/commands/configure.py +729 -113
  13. glaip_sdk/cli/commands/mcps.py +241 -72
  14. glaip_sdk/cli/commands/models.py +11 -5
  15. glaip_sdk/cli/commands/tools.py +49 -57
  16. glaip_sdk/cli/commands/transcripts.py +755 -0
  17. glaip_sdk/cli/config.py +48 -4
  18. glaip_sdk/cli/constants.py +38 -0
  19. glaip_sdk/cli/context.py +8 -0
  20. glaip_sdk/cli/core/__init__.py +79 -0
  21. glaip_sdk/cli/core/context.py +124 -0
  22. glaip_sdk/cli/core/output.py +846 -0
  23. glaip_sdk/cli/core/prompting.py +649 -0
  24. glaip_sdk/cli/core/rendering.py +187 -0
  25. glaip_sdk/cli/display.py +41 -20
  26. glaip_sdk/cli/hints.py +57 -0
  27. glaip_sdk/cli/io.py +6 -3
  28. glaip_sdk/cli/main.py +228 -119
  29. glaip_sdk/cli/masking.py +21 -33
  30. glaip_sdk/cli/pager.py +9 -10
  31. glaip_sdk/cli/parsers/__init__.py +1 -3
  32. glaip_sdk/cli/slash/__init__.py +0 -9
  33. glaip_sdk/cli/slash/accounts_controller.py +500 -0
  34. glaip_sdk/cli/slash/accounts_shared.py +75 -0
  35. glaip_sdk/cli/slash/agent_session.py +58 -20
  36. glaip_sdk/cli/slash/prompt.py +10 -0
  37. glaip_sdk/cli/slash/remote_runs_controller.py +566 -0
  38. glaip_sdk/cli/slash/session.py +736 -134
  39. glaip_sdk/cli/slash/tui/__init__.py +9 -0
  40. glaip_sdk/cli/slash/tui/accounts.tcss +86 -0
  41. glaip_sdk/cli/slash/tui/accounts_app.py +872 -0
  42. glaip_sdk/cli/slash/tui/background_tasks.py +72 -0
  43. glaip_sdk/cli/slash/tui/loading.py +58 -0
  44. glaip_sdk/cli/slash/tui/remote_runs_app.py +628 -0
  45. glaip_sdk/cli/transcript/__init__.py +12 -52
  46. glaip_sdk/cli/transcript/cache.py +255 -44
  47. glaip_sdk/cli/transcript/capture.py +66 -1
  48. glaip_sdk/cli/transcript/history.py +815 -0
  49. glaip_sdk/cli/transcript/viewer.py +70 -463
  50. glaip_sdk/cli/update_notifier.py +14 -5
  51. glaip_sdk/cli/utils.py +243 -1258
  52. glaip_sdk/cli/validators.py +5 -6
  53. glaip_sdk/client/__init__.py +2 -1
  54. glaip_sdk/client/_agent_payloads.py +45 -9
  55. glaip_sdk/client/agent_runs.py +147 -0
  56. glaip_sdk/client/agents.py +287 -29
  57. glaip_sdk/client/base.py +1 -0
  58. glaip_sdk/client/main.py +19 -10
  59. glaip_sdk/client/mcps.py +122 -12
  60. glaip_sdk/client/run_rendering.py +133 -90
  61. glaip_sdk/client/shared.py +21 -0
  62. glaip_sdk/client/tools.py +153 -10
  63. glaip_sdk/config/constants.py +11 -0
  64. glaip_sdk/mcps/__init__.py +21 -0
  65. glaip_sdk/mcps/base.py +345 -0
  66. glaip_sdk/models/__init__.py +90 -0
  67. glaip_sdk/models/agent.py +47 -0
  68. glaip_sdk/models/agent_runs.py +116 -0
  69. glaip_sdk/models/common.py +42 -0
  70. glaip_sdk/models/mcp.py +33 -0
  71. glaip_sdk/models/tool.py +33 -0
  72. glaip_sdk/payload_schemas/__init__.py +1 -13
  73. glaip_sdk/registry/__init__.py +55 -0
  74. glaip_sdk/registry/agent.py +164 -0
  75. glaip_sdk/registry/base.py +139 -0
  76. glaip_sdk/registry/mcp.py +253 -0
  77. glaip_sdk/registry/tool.py +238 -0
  78. glaip_sdk/rich_components.py +58 -2
  79. glaip_sdk/tools/__init__.py +22 -0
  80. glaip_sdk/tools/base.py +435 -0
  81. glaip_sdk/utils/__init__.py +58 -12
  82. glaip_sdk/utils/bundler.py +267 -0
  83. glaip_sdk/utils/client.py +111 -0
  84. glaip_sdk/utils/client_utils.py +39 -7
  85. glaip_sdk/utils/datetime_helpers.py +58 -0
  86. glaip_sdk/utils/discovery.py +78 -0
  87. glaip_sdk/utils/display.py +23 -15
  88. glaip_sdk/utils/export.py +143 -0
  89. glaip_sdk/utils/general.py +0 -33
  90. glaip_sdk/utils/import_export.py +12 -7
  91. glaip_sdk/utils/import_resolver.py +492 -0
  92. glaip_sdk/utils/instructions.py +101 -0
  93. glaip_sdk/utils/rendering/__init__.py +115 -1
  94. glaip_sdk/utils/rendering/formatting.py +5 -30
  95. glaip_sdk/utils/rendering/layout/__init__.py +64 -0
  96. glaip_sdk/utils/rendering/{renderer → layout}/panels.py +9 -0
  97. glaip_sdk/utils/rendering/{renderer → layout}/progress.py +70 -1
  98. glaip_sdk/utils/rendering/layout/summary.py +74 -0
  99. glaip_sdk/utils/rendering/layout/transcript.py +606 -0
  100. glaip_sdk/utils/rendering/models.py +1 -0
  101. glaip_sdk/utils/rendering/renderer/__init__.py +9 -47
  102. glaip_sdk/utils/rendering/renderer/base.py +241 -1434
  103. glaip_sdk/utils/rendering/renderer/config.py +1 -5
  104. glaip_sdk/utils/rendering/renderer/debug.py +26 -20
  105. glaip_sdk/utils/rendering/renderer/factory.py +138 -0
  106. glaip_sdk/utils/rendering/renderer/stream.py +4 -33
  107. glaip_sdk/utils/rendering/renderer/summary_window.py +79 -0
  108. glaip_sdk/utils/rendering/renderer/thinking.py +273 -0
  109. glaip_sdk/utils/rendering/renderer/tool_panels.py +442 -0
  110. glaip_sdk/utils/rendering/renderer/transcript_mode.py +162 -0
  111. glaip_sdk/utils/rendering/state.py +204 -0
  112. glaip_sdk/utils/rendering/steps/__init__.py +34 -0
  113. glaip_sdk/utils/rendering/{steps.py → steps/event_processor.py} +53 -440
  114. glaip_sdk/utils/rendering/steps/format.py +176 -0
  115. glaip_sdk/utils/rendering/steps/manager.py +387 -0
  116. glaip_sdk/utils/rendering/timing.py +36 -0
  117. glaip_sdk/utils/rendering/viewer/__init__.py +21 -0
  118. glaip_sdk/utils/rendering/viewer/presenter.py +184 -0
  119. glaip_sdk/utils/resource_refs.py +25 -13
  120. glaip_sdk/utils/runtime_config.py +306 -0
  121. glaip_sdk/utils/serialization.py +18 -0
  122. glaip_sdk/utils/sync.py +142 -0
  123. glaip_sdk/utils/validation.py +16 -24
  124. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/METADATA +39 -4
  125. glaip_sdk-0.6.5b3.dist-info/RECORD +145 -0
  126. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/WHEEL +1 -1
  127. glaip_sdk/models.py +0 -240
  128. glaip_sdk-0.1.2.dist-info/RECORD +0 -82
  129. {glaip_sdk-0.1.2.dist-info → glaip_sdk-0.6.5b3.dist-info}/entry_points.txt +0 -0
glaip_sdk/client/mcps.py CHANGED
@@ -3,18 +3,22 @@
3
3
 
4
4
  Authors:
5
5
  Raymond Christopher (raymond.christopher@gdplabs.id)
6
+ Christian Trisno Sen Long Chen (christian.t.s.l.chen@gdplabs.id)
6
7
  """
7
8
 
8
9
  import logging
9
10
  from typing import Any
10
11
 
11
12
  from glaip_sdk.client.base import BaseClient
12
- from glaip_sdk.config.constants import (
13
- DEFAULT_MCP_TRANSPORT,
14
- DEFAULT_MCP_TYPE,
13
+ from glaip_sdk.config.constants import DEFAULT_MCP_TRANSPORT, DEFAULT_MCP_TYPE
14
+ from glaip_sdk.mcps import MCP
15
+ from glaip_sdk.models import MCPResponse
16
+ from glaip_sdk.utils.client_utils import (
17
+ add_kwargs_to_payload,
18
+ create_model_instances,
19
+ find_by_name,
15
20
  )
16
- from glaip_sdk.models import MCP
17
- from glaip_sdk.utils.client_utils import create_model_instances, find_by_name
21
+ from glaip_sdk.utils.resource_refs import is_uuid
18
22
 
19
23
  # API endpoints
20
24
  MCPS_ENDPOINT = "/mcps/"
@@ -45,7 +49,8 @@ class MCPClient(BaseClient):
45
49
  def get_mcp_by_id(self, mcp_id: str) -> MCP:
46
50
  """Get MCP by ID."""
47
51
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}")
48
- return MCP(**data)._set_client(self)
52
+ response = MCPResponse(**data)
53
+ return MCP.from_response(response, client=self)
49
54
 
50
55
  def find_mcps(self, name: str | None = None) -> list[MCP]:
51
56
  """Find MCPs by name."""
@@ -77,7 +82,8 @@ class MCPClient(BaseClient):
77
82
  get_endpoint_fmt=f"{MCPS_ENDPOINT}{{id}}",
78
83
  json=payload,
79
84
  )
80
- return MCP(**full_mcp_data)._set_client(self)
85
+ response = MCPResponse(**full_mcp_data)
86
+ return MCP.from_response(response, client=self)
81
87
 
82
88
  def update_mcp(self, mcp_id: str, **kwargs) -> MCP:
83
89
  """Update an existing MCP.
@@ -99,12 +105,102 @@ class MCPClient(BaseClient):
99
105
  method = "PATCH"
100
106
 
101
107
  data = self._request(method, f"{MCPS_ENDPOINT}{mcp_id}", json=kwargs)
102
- return MCP(**data)._set_client(self)
108
+ response = MCPResponse(**data)
109
+ return MCP.from_response(response, client=self)
103
110
 
104
111
  def delete_mcp(self, mcp_id: str) -> None:
105
112
  """Delete an MCP."""
106
113
  self._request("DELETE", f"{MCPS_ENDPOINT}{mcp_id}")
107
114
 
115
+ def upsert_mcp(
116
+ self,
117
+ identifier: str | MCP,
118
+ description: str | None = None,
119
+ config: dict[str, Any] | None = None,
120
+ **kwargs,
121
+ ) -> MCP:
122
+ """Create or update an MCP by instance, ID, or name.
123
+
124
+ Args:
125
+ identifier: MCP instance, ID (UUID string), or name
126
+ description: MCP description
127
+ config: MCP configuration dictionary
128
+ **kwargs: Additional parameters (transport, metadata, etc.)
129
+
130
+ Returns:
131
+ The created or updated MCP.
132
+
133
+ Example:
134
+ >>> # By name (creates if not exists)
135
+ >>> mcp = client.mcps.upsert_mcp(
136
+ ... "deepwiki",
137
+ ... transport="sse",
138
+ ... config={"url": "https://mcp.deepwiki.com/sse"},
139
+ ... )
140
+ >>> # By instance
141
+ >>> mcp = client.mcps.upsert_mcp(existing_mcp, description="Updated")
142
+ >>> # By ID
143
+ >>> mcp = client.mcps.upsert_mcp("uuid-here", description="Updated")
144
+ """
145
+ # Handle MCP instance
146
+ if isinstance(identifier, MCP):
147
+ if identifier.id:
148
+ logger.info("Updating MCP by instance: %s", identifier.name)
149
+ return self._do_upsert_update(identifier.id, identifier.name, description, config, **kwargs)
150
+ # MCP without ID - treat name as identifier
151
+ identifier = identifier.name
152
+
153
+ # Handle string (ID or name)
154
+ if isinstance(identifier, str):
155
+ if is_uuid(identifier):
156
+ logger.info("Updating MCP by ID: %s", identifier)
157
+ existing = self.get_mcp_by_id(identifier)
158
+ return self._do_upsert_update(identifier, existing.name, description, config, **kwargs)
159
+
160
+ # It's a name - find or create
161
+ return self._upsert_by_name(identifier, description, config, **kwargs)
162
+
163
+ raise ValueError(f"Invalid identifier type: {type(identifier)}")
164
+
165
+ def _do_upsert_update(
166
+ self,
167
+ mcp_id: str,
168
+ name: str | None,
169
+ description: str | None,
170
+ config: dict[str, Any] | None,
171
+ **kwargs,
172
+ ) -> MCP:
173
+ """Perform the update part of upsert."""
174
+ update_kwargs = {**kwargs}
175
+ if name is not None:
176
+ update_kwargs["name"] = name
177
+ if description is not None:
178
+ update_kwargs["description"] = description
179
+ if config is not None:
180
+ update_kwargs["config"] = config
181
+ return self.update_mcp(mcp_id, **update_kwargs)
182
+
183
+ def _upsert_by_name(
184
+ self,
185
+ name: str,
186
+ description: str | None,
187
+ config: dict[str, Any] | None,
188
+ **kwargs,
189
+ ) -> MCP:
190
+ """Find by name and update, or create if not found."""
191
+ existing = self.find_mcps(name)
192
+
193
+ if len(existing) == 1:
194
+ logger.info("Updating existing MCP: %s", name)
195
+ return self._do_upsert_update(existing[0].id, name, description, config, **kwargs)
196
+
197
+ if len(existing) > 1:
198
+ raise ValueError(f"Multiple MCPs found with name '{name}'")
199
+
200
+ # Create new MCP
201
+ logger.info("Creating new MCP: %s", name)
202
+ return self.create_mcp(name=name, description=description, config=config, **kwargs)
203
+
108
204
  def _build_create_payload(
109
205
  self,
110
206
  name: str,
@@ -147,9 +243,7 @@ class MCPClient(BaseClient):
147
243
 
148
244
  # Add any other kwargs (excluding already handled ones)
149
245
  excluded_keys = {"type"} # type is handled above
150
- for key, value in kwargs.items():
151
- if key not in excluded_keys:
152
- payload[key] = value
246
+ add_kwargs_to_payload(payload, kwargs, excluded_keys)
153
247
 
154
248
  return payload
155
249
 
@@ -206,7 +300,23 @@ class MCPClient(BaseClient):
206
300
  def get_mcp_tools(self, mcp_id: str) -> list[dict[str, Any]]:
207
301
  """Get tools available from an MCP."""
208
302
  data = self._request("GET", f"{MCPS_ENDPOINT}{mcp_id}/tools")
209
- return data or []
303
+ if data is None:
304
+ return []
305
+ if isinstance(data, list):
306
+ return data
307
+ if isinstance(data, dict):
308
+ if "tools" in data:
309
+ return data.get("tools", []) or []
310
+ logger.warning(
311
+ "Unexpected MCP tools response keys %s; returning empty list",
312
+ list(data.keys()),
313
+ )
314
+ return []
315
+ logger.warning(
316
+ "Unexpected MCP tools response type %s; returning empty list",
317
+ type(data).__name__,
318
+ )
319
+ return []
210
320
 
211
321
  def test_mcp_connection(self, config: dict[str, Any]) -> dict[str, Any]:
212
322
  """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)
104
83
 
105
- return self._create_default_renderer(verbose)
84
+ if verbose:
85
+ return self._attach_buffer(base_options.build(make_verbose_renderer), transcript_buffer)
86
+
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,49 +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
- silent_config = RendererConfig(
128
- live=False,
129
- persist_live=False,
130
- render_thinking=False,
131
- )
132
- return RichStreamRenderer(
133
- console=_Console(file=io.StringIO(), force_terminal=False),
134
- cfg=silent_config,
135
- verbose=False,
136
- )
137
-
138
- def _create_minimal_renderer(self) -> RichStreamRenderer:
139
- minimal_config = RendererConfig(
140
- live=False,
141
- persist_live=False,
142
- render_thinking=False,
143
- )
144
- return RichStreamRenderer(
145
- console=_Console(),
146
- cfg=minimal_config,
147
- verbose=False,
148
- )
149
-
150
- def _create_verbose_renderer(self) -> RichStreamRenderer:
151
- verbose_config = RendererConfig(
152
- theme="dark",
153
- style="debug",
154
- live=False,
155
- append_finished_snapshots=False,
156
- )
157
- return RichStreamRenderer(
158
- console=_Console(),
159
- cfg=verbose_config,
160
- verbose=True,
161
- )
162
-
163
- def _create_default_renderer(self, verbose: bool) -> RichStreamRenderer:
164
- if verbose:
165
- return self._create_verbose_renderer()
166
- default_config = RendererConfig()
167
- return RichStreamRenderer(console=_Console(), cfg=default_config)
168
-
169
122
  # --------------------------------------------------------------------- #
170
123
  # Streaming event handling
171
124
  # --------------------------------------------------------------------- #
@@ -216,12 +169,27 @@ class AgentRunRenderingManager:
216
169
  meta: dict[str, Any],
217
170
  renderer: RichStreamRenderer,
218
171
  ) -> None:
172
+ """Capture request ID from response headers and update metadata.
173
+
174
+ Args:
175
+ stream_response: HTTP response stream.
176
+ meta: Metadata dictionary to update.
177
+ renderer: Renderer instance.
178
+ """
219
179
  req_id = stream_response.headers.get("x-request-id") or stream_response.headers.get("x-run-id")
220
180
  if req_id:
221
181
  meta["run_id"] = req_id
222
182
  renderer.on_start(meta)
223
183
 
224
184
  def _maybe_start_timer(self, event: dict[str, Any]) -> float | None:
185
+ """Start timing if this is a content-bearing event.
186
+
187
+ Args:
188
+ event: Event dictionary.
189
+
190
+ Returns:
191
+ Monotonic time if timer should start, None otherwise.
192
+ """
225
193
  try:
226
194
  ev = json.loads(event["data"])
227
195
  except json.JSONDecodeError:
@@ -239,6 +207,18 @@ class AgentRunRenderingManager:
239
207
  stats_usage: dict[str, Any],
240
208
  meta: dict[str, Any],
241
209
  ) -> tuple[str, dict[str, Any]]:
210
+ """Process a single streaming event.
211
+
212
+ Args:
213
+ event: Event dictionary.
214
+ renderer: Renderer instance.
215
+ final_text: Accumulated text so far.
216
+ stats_usage: Usage statistics dictionary.
217
+ meta: Metadata dictionary.
218
+
219
+ Returns:
220
+ Tuple of (updated_final_text, updated_stats_usage).
221
+ """
242
222
  try:
243
223
  ev = json.loads(event["data"])
244
224
  except json.JSONDecodeError:
@@ -294,6 +274,15 @@ class AgentRunRenderingManager:
294
274
  return None
295
275
 
296
276
  def _handle_content_event(self, ev: dict[str, Any], final_text: str) -> str:
277
+ """Handle a content event and update final text.
278
+
279
+ Args:
280
+ ev: Event dictionary.
281
+ final_text: Current accumulated text.
282
+
283
+ Returns:
284
+ Updated final text.
285
+ """
297
286
  content = ev.get("content", "")
298
287
  if not content.startswith("Artifact received:"):
299
288
  return content
@@ -305,6 +294,13 @@ class AgentRunRenderingManager:
305
294
  meta: dict[str, Any],
306
295
  renderer: RichStreamRenderer,
307
296
  ) -> None:
297
+ """Handle a run_info event and update metadata.
298
+
299
+ Args:
300
+ ev: Event dictionary.
301
+ meta: Metadata dictionary to update.
302
+ renderer: Renderer instance.
303
+ """
308
304
  if ev.get("model"):
309
305
  meta["model"] = ev["model"]
310
306
  renderer.on_start(meta)
@@ -318,7 +314,52 @@ class AgentRunRenderingManager:
318
314
  return
319
315
 
320
316
  text_value = _coerce_to_string(text)
321
- _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
322
363
 
323
364
  # --------------------------------------------------------------------- #
324
365
  # Finalisation helpers
@@ -345,7 +386,9 @@ class AgentRunRenderingManager:
345
386
  elif hasattr(renderer, "buffer"):
346
387
  buffer_values = renderer.buffer
347
388
 
348
- 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:
349
392
  try:
350
393
  rendered_text = "".join(buffer_values)
351
394
  except TypeError:
@@ -356,7 +399,7 @@ class AgentRunRenderingManager:
356
399
  self._ensure_renderer_final_content(renderer, fallback_text)
357
400
 
358
401
  renderer.on_complete(st)
359
- return final_text or rendered_text or "No response content received."
402
+ return final_text or rendered_text or NO_AGENT_RESPONSE_FALLBACK
360
403
 
361
404
 
362
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
+ }